diff --git a/dependencies.gradle b/dependencies.gradle index c3829696..374bcf22 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -35,7 +35,7 @@ */ dependencies { // ========================= Real Deps ========================= // - implementation("com.github.GTNewHorizons:GTNHLib:0.11.2:dev") + implementation("com.github.GTNewHorizons:GTNHLib:0.11.3:dev") implementation("com.github.GTNewHorizons:RegionLib:v0.1.0-GTNH:dev") compileOnly("org.jetbrains:annotations:26.0.2") @@ -47,6 +47,7 @@ dependencies { compileOnly("com.falsepattern:chunkapi-mc1.7.10:0.8.1:dev") compileOnly("com.falsepattern:endlessids-mc1.7.10:1.7.1:dev") compileOnly("ganymedes01.etfuturum:Et-Futurum-Requiem:2.6.2.21-GTNH-daily:dev") + compileOnly("com.github.GTNewHorizons:Angelica:2.1.28:dev") // ========================= Test Deps ========================= // devOnlyNonPublishable("com.github.GTNewHorizons:NotEnoughItems:2.8.100-GTNH:dev") diff --git a/src/buildSrc/java/com/cardinalstar/cubicchunks/codegen/FieldType.java b/src/buildSrc/java/com/cardinalstar/cubicchunks/codegen/FieldType.java new file mode 100644 index 00000000..10dcd088 --- /dev/null +++ b/src/buildSrc/java/com/cardinalstar/cubicchunks/codegen/FieldType.java @@ -0,0 +1,72 @@ +package com.cardinalstar.cubicchunks.codegen; + +public enum FieldType { + + i32, + u32, + i64, + u64, + f32, + f64; + + public String javaType() { + switch (this) { + case i32: + case u32: + return "int"; + case i64: + case u64: + return "long"; + case f32: + return "float"; + case f64: + return "double"; + default: + throw new IllegalArgumentException(); + } + } + + public String glslType() { + switch (this) { + case i32: + return "int"; + case u32: + return "uint"; + case i64: + return "long"; + case u64: + return "ulong"; + case f32: + return "float"; + case f64: + return "double"; + default: + throw new IllegalArgumentException(); + } + } + + public int wordWidth() { + switch (this) { + case i32: + case u32: + case f32: + return 1; + case i64: + case u64: + case f64: + return 2; + default: + throw new IllegalArgumentException(); + } + } + + /** Byte size of this type. */ + public int byteSize() { + return wordWidth() * 4; + } + + /** std430 alignment requirement in bytes. */ + public int glAlign() { + return wordWidth() == 2 ? 8 : 4; + } +} diff --git a/src/buildSrc/java/com/cardinalstar/cubicchunks/codegen/GenerateStructs.java b/src/buildSrc/java/com/cardinalstar/cubicchunks/codegen/GenerateStructs.java new file mode 100644 index 00000000..e319ab2b --- /dev/null +++ b/src/buildSrc/java/com/cardinalstar/cubicchunks/codegen/GenerateStructs.java @@ -0,0 +1,46 @@ +package com.cardinalstar.cubicchunks.codegen; + +import java.io.IOException; + +public class GenerateStructs { + + public static void main(String[] args) throws IOException { + NOISE2D_UNIFORM.writeAllGL(); + ENHANCED_BLOCK_PICKER.writeAllGL(); + BIOME_DISTANCE.writeAllGL(); + BIOME_LOOKUP.writeAllGL(); + HEIGHTMAP.writeAllGL(); + } + + private static final Struct NOISE2D_UNIFORM = new Struct( + "com.cardinalstar.cubicchunks.api.worldgen.hwaccel", + "Noise2DUniform").addField(FieldType.i32, "offsetX") + .addField(FieldType.i32, "offsetZ") + .addField(FieldType.u32, "outputOffset"); + + private static final Struct ENHANCED_BLOCK_PICKER = new Struct( + "com.cardinalstar.cubicchunks.worldgen.ccenhanced", + "BlockPickerUniform").addField(FieldType.i32, "cubeY") + .addField(FieldType.u32, "heightmapOffset") + .addField(FieldType.u32, "blockOffset"); + + private static final Struct BIOME_DISTANCE = new Struct( + "com.cardinalstar.cubicchunks.worldgen.ccenhanced.biome", + "BiomeDistanceUniform").addField(FieldType.u32, "temperatureOffset") + .addField(FieldType.u32, "humidityOffset") + .addField(FieldType.u32, "continentalnessOffset") + .addField(FieldType.u32, "erosionOffset") + .addField(FieldType.u32, "distanceOffset"); + + private static final Struct BIOME_LOOKUP = new Struct( + "com.cardinalstar.cubicchunks.worldgen.ccenhanced.biome", + "BiomeLookupUniform").addField(FieldType.u32, "distanceOffset") + .addField(FieldType.u32, "closestOffset") + .addField(FieldType.u32, "weightsOffset"); + + private static final Struct HEIGHTMAP = new Struct( + "com.cardinalstar.cubicchunks.worldgen.ccenhanced.biome", + "HeightMapUniform").addField(FieldType.u32, "distancesOffset") + .addField(FieldType.u32, "hvNoiseOffset") + .addField(FieldType.u32, "heightMapOffset"); +} diff --git a/src/buildSrc/java/com/cardinalstar/cubicchunks/codegen/Struct.java b/src/buildSrc/java/com/cardinalstar/cubicchunks/codegen/Struct.java new file mode 100644 index 00000000..e9b82ae1 --- /dev/null +++ b/src/buildSrc/java/com/cardinalstar/cubicchunks/codegen/Struct.java @@ -0,0 +1,477 @@ +package com.cardinalstar.cubicchunks.codegen; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class Struct { + + public final String pkg, name; + + public final List fields = new ArrayList<>(); + /** Total word count across all fields (Java IntBuffer stride). */ + public int stride = 0; + /** Total byte size with std430 alignment padding. */ + public int glByteStride = 0; + + public Struct(String pkg, String name) { + this.pkg = pkg; + this.name = name; + } + + public Struct addField(FieldType type, String name) { + int glByteOffset = ((glByteStride + type.glAlign() - 1) / type.glAlign()) * type.glAlign(); + fields.add(new StructField(type, name, stride, glByteOffset)); + stride += type.wordWidth(); + glByteStride = glByteOffset + type.byteSize(); + return this; + } + + public void writeAllGL() throws IOException { + writePrimitiveBuffer(); + writePrimitiveView(); + writeGLStruct(); + } + + public void writePrimitiveBuffer() throws IOException { + StringBuilder code = new StringBuilder(); + + String viewName = this.name + "PrimitiveView"; + String bufferName = this.name + "PrimitiveBuffer"; + + code.append("package ") + .append(pkg) + .append(";\n"); + code.append("\n"); + code.append("import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.PrimitiveBuffer;\n"); + code.append("import com.cardinalstar.cubicchunks.util.ObjectPooler;\n"); + code.append("import gnu.trove.procedure.TIntObjectProcedure;\n"); + code.append("\n"); + code.append("import java.nio.ByteBuffer;\n"); + code.append("import java.nio.ByteOrder;\n"); + code.append("import java.nio.IntBuffer;\n"); + code.append("import java.util.Arrays;\n"); + code.append("import java.util.Iterator;\n"); + code.append("import java.util.NoSuchElementException;\n"); + code.append("\n"); + code.append("public class ") + .append(bufferName) + .append(" implements PrimitiveBuffer<") + .append(viewName) + .append("> {\n"); + code.append("\n"); + code.append(" public static final int INT_STRIDE = ") + .append(this.stride) + .append(";\n"); + code.append("\n"); + code.append(" private final byte[] rawData;\n"); + code.append(" private final ByteBuffer bytes;\n"); + code.append(" private final IntBuffer data;\n"); + code.append(" private final int size;\n"); + code.append(" private final ObjectPooler<") + .append(viewName) + .append("> pool;\n"); + code.append("\n"); + code.append(" public ") + .append(bufferName) + .append("(int size) {\n"); + code.append(" this.size = size;\n"); + code.append(" this.rawData = new byte[size * INT_STRIDE * 4];\n"); + code.append(" this.bytes = ByteBuffer.wrap(this.rawData).order(ByteOrder.nativeOrder());\n"); + code.append(" this.data = this.bytes.asIntBuffer();\n"); + code.append(" this.pool = new ObjectPooler<>(this::newView);\n"); + code.append(" }\n"); + code.append("\n"); + code.append(" private ") + .append(viewName) + .append(" newView() {\n"); + code.append(" return new ") + .append(viewName) + .append("(pool, this.data);\n"); + code.append(" }\n"); + code.append("\n"); + code.append(" @Override\n"); + code.append(" public void clear() {\n"); + code.append(" Arrays.fill(this.rawData, (byte) 0);\n"); + code.append(" }\n"); + code.append("\n"); + code.append(" @Override\n"); + code.append(" public int size() {\n"); + code.append(" return this.size;\n"); + code.append(" }\n"); + code.append("\n"); + code.append(" @Override\n"); + code.append(" public int getByteLength() {\n"); + code.append(" return this.size * INT_STRIDE * 4;\n"); + code.append(" }\n"); + code.append("\n"); + code.append(" @Override\n"); + code.append(" public ") + .append(viewName) + .append(" get(int index) {\n"); + code.append(" ") + .append(viewName) + .append(" view = this.pool.getInstance();\n"); + code.append(" view.initialize(index);\n"); + code.append(" return view;\n"); + code.append(" }\n"); + code.append("\n"); + code.append(" @Override\n"); + code.append(" public void forEachSlow(TIntObjectProcedure<") + .append(viewName) + .append("> fn) {\n"); + code.append(" for (int i = 0; i < this.size; i++) {\n"); + code.append(" ") + .append(viewName) + .append(" view = new ") + .append(viewName) + .append("(null, this.data);\n"); + code.append(" view.initialize(i);\n"); + code.append(" fn.execute(i, view);\n"); + code.append(" }\n"); + code.append(" }\n"); + code.append("\n"); + code.append(" @Override\n"); + code.append(" public void forEachFast(TIntObjectProcedure<") + .append(viewName) + .append("> fn) {\n"); + code.append(" ") + .append(viewName) + .append(" view = new ") + .append(viewName) + .append("(null, this.data);\n"); + code.append(" for (int i = 0; i < this.size; i++) {\n"); + code.append(" view.initialize(i);\n"); + code.append(" fn.execute(i, view);\n"); + code.append(" }\n"); + code.append(" }\n"); + code.append("\n"); + code.append(" @Override\n"); + code.append(" public Iterator<") + .append(viewName) + .append("> iteratorSlow() {\n"); + code.append(" return new Iterator<") + .append(viewName) + .append(">() {\n"); + code.append(" private int i = 0;\n"); + code.append("\n"); + code.append(" @Override\n"); + code.append(" public boolean hasNext() {\n"); + code.append(" return i < size;\n"); + code.append(" }\n"); + code.append("\n"); + code.append(" @Override\n"); + code.append(" public ") + .append(viewName) + .append(" next() {\n"); + code.append(" if (!hasNext()) throw new NoSuchElementException();\n"); + code.append(" ") + .append(viewName) + .append(" view = new ") + .append(viewName) + .append("(null, data);\n"); + code.append(" view.initialize(i++);\n"); + code.append(" return view;\n"); + code.append(" }\n"); + code.append(" };\n"); + code.append(" }\n"); + code.append("\n"); + code.append(" @Override\n"); + code.append(" public Iterator<") + .append(viewName) + .append("> iteratorFast() {\n"); + code.append(" return new Iterator<") + .append(viewName) + .append(">() {\n"); + code.append(" private int i = 0;\n"); + code.append(" private final ") + .append(viewName) + .append(" view = new ") + .append(viewName) + .append("(null, data);\n"); + code.append("\n"); + code.append(" @Override\n"); + code.append(" public boolean hasNext() {\n"); + code.append(" return i < size;\n"); + code.append(" }\n"); + code.append("\n"); + code.append(" @Override\n"); + code.append(" public ") + .append(viewName) + .append(" next() {\n"); + code.append(" if (!hasNext()) throw new NoSuchElementException();\n"); + code.append(" view.initialize(i++);\n"); + code.append(" return view;\n"); + code.append(" }\n"); + code.append(" };\n"); + code.append(" }\n"); + code.append("\n"); + code.append(" @Override\n"); + code.append(" public void upload(ByteBuffer dest) {\n"); + code.append(" if (dest.remaining() != this.rawData.length) {\n"); + code.append( + " throw new IllegalArgumentException(\"Byte length mismatch: expected \" + this.rawData.length + \" but got \" + dest.remaining());\n"); + code.append(" }\n"); + code.append(" dest.put(this.rawData, 0, this.rawData.length);\n"); + code.append(" }\n"); + code.append("\n"); + code.append(" @Override\n"); + code.append(" public void download(ByteBuffer source) {\n"); + code.append(" if (source.remaining() != this.rawData.length) {\n"); + code.append( + " throw new IllegalArgumentException(\"Byte length mismatch: expected \" + this.rawData.length + \" but got \" + source.remaining());\n"); + code.append(" }\n"); + code.append(" source.get(this.rawData, 0, this.rawData.length);\n"); + code.append(" }\n"); + code.append("\n"); + code.append("}\n"); + + Path src = Paths.get("build", "generated", "sources", "structs") + .toAbsolutePath(); + String[] pkg = this.pkg.split("\\."); + + Path next = Paths.get(pkg[0], Arrays.copyOfRange(pkg, 1, pkg.length)); + + Files.createDirectories(src.resolve(next)); + + Path file = Paths.get(name + "PrimitiveBuffer.java"); + + Files.write( + src.resolve(next) + .resolve(file), + code.toString() + .getBytes(StandardCharsets.UTF_8)); + } + + public void writePrimitiveView() throws IOException { + StringBuilder code = new StringBuilder(); + + String name = this.name + "PrimitiveView"; + + code.append("package ") + .append(pkg) + .append(";\n"); + code.append("\n"); + code.append("import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.PrimitiveView;\n"); + code.append("import com.cardinalstar.cubicchunks.util.ObjectPooler;\n"); + code.append("\n"); + code.append("import java.nio.IntBuffer;\n"); + code.append("@SuppressWarnings({ \"UnnecessaryLocalVariable\", \"PointlessArithmeticExpression\" })\n"); + code.append("public final class ") + .append(name) + .append(" implements PrimitiveView {\n"); + code.append(" private int index;\n"); + code.append(" private final ObjectPooler<") + .append(name) + .append("> pool;\n"); + code.append(" private boolean alive;\n"); + code.append(" private final IntBuffer data;\n"); + code.append("\n"); + code.append(" ") + .append(name) + .append("(ObjectPooler<") + .append(name) + .append("> pool, IntBuffer data) {\n"); + code.append(" this.pool = pool;\n"); + code.append(" this.data = data;\n"); + code.append(" }\n"); + code.append("\n"); + code.append(" @Override\n"); + code.append(" public void close() {\n"); + code.append(" if (this.pool != null) {\n"); + code.append(" this.pool.releaseInstance(this);\n"); + code.append(" }\n"); + code.append(" \n"); + code.append(" this.alive = false;\n"); + code.append(" }\n"); + code.append("\n"); + code.append(" @Override\n"); + code.append(" public int getIndex() {\n"); + code.append(" return this.index;\n"); + code.append(" }\n"); + code.append("\n"); + code.append(" private void assertAlive() {\n"); + code.append(" if (!this.alive) throw new IllegalStateException(this + \" is not alive\");\n"); + code.append(" }\n"); + code.append("\n"); + code.append(" void initialize(int index) {\n"); + code.append(" this.alive = true;\n"); + code.append(" this.index = index;\n"); + code.append(" }\n"); + code.append("\n"); + + for (StructField field : this.fields) { + code.append(" public ") + .append(field.type.javaType()) + .append(" get") + .append(field.getPascalCase()) + .append("() {\n"); + code.append(" assertAlive();\n"); + + if (field.type.wordWidth() == 2) { + code.append(" long lower = this.data.get(index * ") + .append(stride) + .append(" + ") + .append(field.wordOffset) + .append(");\n"); + code.append(" long upper = this.data.get(index * ") + .append(stride) + .append(" + ") + .append(field.wordOffset + 1) + .append(");\n"); + code.append(" long bits = lower | (upper << 32);\n"); + + if (field.type == FieldType.f64) { + code.append(" return Double.longBitsToDouble(bits);\n"); + } else { + code.append(" return bits;\n"); + } + } else { + code.append(" int bits = this.data.get(index * ") + .append(stride) + .append(" + ") + .append(field.wordOffset) + .append(");\n"); + + if (field.type == FieldType.f32) { + code.append(" return Float.intBitsToFloat(bits);\n"); + } else { + code.append(" return bits;\n"); + } + } + + code.append(" }\n"); + code.append("\n"); + + code.append(" public ") + .append(name) + .append(" set") + .append(field.getPascalCase()) + .append("(") + .append(field.type.javaType()) + .append(" value) {\n"); + code.append(" assertAlive();\n"); + + if (field.type.wordWidth() == 2) { + if (field.type == FieldType.f64) { + code.append(" long bits = Double.doubleToLongBits(value);\n"); + } else { + code.append(" long bits = value;\n"); + } + + code.append(" this.data.put(index * ") + .append(stride) + .append(" + ") + .append(field.wordOffset) + .append(", (int) bits);\n"); + code.append(" this.data.put(index * ") + .append(stride) + .append(" + ") + .append(field.wordOffset + 1) + .append(", (int) (bits >> 32));\n"); + } else { + if (field.type == FieldType.f32) { + code.append(" int bits = Float.floatToIntBits(value);\n"); + } else { + code.append(" int bits = value;\n"); + } + + code.append(" this.data.put(index * ") + .append(stride) + .append(" + ") + .append(field.wordOffset) + .append(", bits);\n"); + } + + code.append(" return this;\n"); + code.append(" }\n"); + code.append("\n"); + } + + code.append("}\n"); + + Path src = Paths.get("build", "generated", "sources", "structs") + .toAbsolutePath(); + String[] pkg = this.pkg.split("\\."); + + Path next = Paths.get(pkg[0], Arrays.copyOfRange(pkg, 1, pkg.length)); + + Files.createDirectories(src.resolve(next)); + + Path file = Paths.get(name + ".java"); + + Files.write( + src.resolve(next) + .resolve(file), + code.toString() + .getBytes(StandardCharsets.UTF_8)); + } + + public void writeGLStruct() throws IOException { + String glStructName = this.name + "GLStruct"; + + // Build GLSL struct, inserting uint padding fields where std430 alignment + // would create a gap vs the Java sequential (word-packed) layout. + StringBuilder glsl = new StringBuilder(); + glsl.append("struct ") + .append(this.name) + .append(" {\n"); + + int cursor = 0; + int padCount = 0; + for (StructField field : this.fields) { + int padBytes = field.glByteOffset - cursor; + for (int i = 0; i < padBytes / 4; i++) { + glsl.append(" uint _pad") + .append(padCount++) + .append(";\n"); + } + glsl.append(" ") + .append(field.type.glslType()) + .append(" ") + .append(field.name) + .append(";\n"); + cursor = field.glByteOffset + field.type.byteSize(); + } + + glsl.append("};\n"); + + // Wrap in a Java class with a text block SOURCE field. + // Indent content by 12 spaces so the closing """ at 12 spaces strips it cleanly. + StringBuilder code = new StringBuilder(); + code.append("package ") + .append(pkg) + .append(";\n"); + code.append("\n"); + code.append("public final class ") + .append(glStructName) + .append(" {\n"); + code.append(" public static final String SOURCE = \"\"\"\n"); + + for (String line : glsl.toString() + .split("\n")) { + code.append(" ") + .append(line) + .append("\n"); + } + + code.append(" \"\"\";\n"); + code.append("}\n"); + + Path src = Paths.get("build", "generated", "sources", "structs") + .toAbsolutePath(); + String[] pkg = this.pkg.split("\\."); + Path next = Paths.get(pkg[0], Arrays.copyOfRange(pkg, 1, pkg.length)); + Files.createDirectories(src.resolve(next)); + Files.write( + src.resolve(next) + .resolve(Paths.get(glStructName + ".java")), + code.toString() + .getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/src/buildSrc/java/com/cardinalstar/cubicchunks/codegen/StructField.java b/src/buildSrc/java/com/cardinalstar/cubicchunks/codegen/StructField.java new file mode 100644 index 00000000..95315e60 --- /dev/null +++ b/src/buildSrc/java/com/cardinalstar/cubicchunks/codegen/StructField.java @@ -0,0 +1,25 @@ +package com.cardinalstar.cubicchunks.codegen; + +import java.util.Locale; + +public class StructField { + + public final FieldType type; + public final String name; + /** Word index into the Java IntBuffer (sequential, no alignment gaps). */ + public final int wordOffset; + /** Byte offset in the GLSL std430 layout (alignment-padded). */ + public final int glByteOffset; + + StructField(FieldType type, String name, int wordOffset, int glByteOffset) { + this.type = type; + this.name = name; + this.wordOffset = wordOffset; + this.glByteOffset = glByteOffset; + } + + public String getPascalCase() { + return name.substring(0, 1) + .toUpperCase(Locale.ROOT) + name.substring(1); + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/CubicChunks.java b/src/main/java/com/cardinalstar/cubicchunks/CubicChunks.java index 96d4c517..87ad7382 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/CubicChunks.java +++ b/src/main/java/com/cardinalstar/cubicchunks/CubicChunks.java @@ -40,6 +40,7 @@ import com.cardinalstar.cubicchunks.api.world.storage.ICubicStorage; import com.cardinalstar.cubicchunks.api.world.storage.StorageFormatFactory; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelContext; import com.cardinalstar.cubicchunks.api.worldtype.VanillaCubicWorldType; import com.cardinalstar.cubicchunks.async.TaskPool; import com.cardinalstar.cubicchunks.event.handlers.ClientEventHandler; @@ -52,6 +53,7 @@ import com.cardinalstar.cubicchunks.util.SideUtils; import com.cardinalstar.cubicchunks.world.worldgen.WorldGenerators; import com.cardinalstar.cubicchunks.worldgen.WorldgenHangWatchdog; +import com.cardinalstar.cubicchunks.worldgen.ccenhanced.CCEnhancedWorldType; import com.falsepattern.chunk.api.DataRegistry; import com.gtnewhorizon.gtnhlib.config.ConfigException; import com.gtnewhorizon.gtnhlib.config.ConfigurationManager; @@ -74,7 +76,7 @@ import cpw.mods.fml.relauncher.Side; @ParametersAreNonnullByDefault -@Mod(modid = CubicChunks.MODID, useMetadata = true, dependencies = "required-after:RegionLib;") +@Mod(modid = CubicChunks.MODID, useMetadata = true, dependencies = "required-after:RegionLib;required-after:gtnhlib;") public class CubicChunks { public static final int MAX_RENDER_DISTANCE = 64; @@ -121,9 +123,17 @@ public void preInit(FMLPreInitializationEvent e) { registerAnvil3dStorageFormatProvider(); VanillaCubicWorldType.init(); + CCEnhancedWorldType.init(); LOGGER.debug("Registered world types"); + if (FMLCommonHandler.instance() + .getSide() == Side.CLIENT) { + KernelContext.initClient(); + } else { + KernelContext.initServer(); + } + try { ConfigurationManager.registerConfig(CubicChunksConfig.class); } catch (ConfigException ex) { diff --git a/src/main/java/com/cardinalstar/cubicchunks/CubicChunksConfig.java b/src/main/java/com/cardinalstar/cubicchunks/CubicChunksConfig.java index bbb5a180..696698a9 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/CubicChunksConfig.java +++ b/src/main/java/com/cardinalstar/cubicchunks/CubicChunksConfig.java @@ -82,9 +82,13 @@ public class CubicChunksConfig { public static int verticalCubeLoadDistance = 8; @Config.LangKey("cubicchunks.config.enable_chunk_debugging") - @Config.Comment("Displays coloured boxes over cubes at Y=8 for debugging purposes.") + @Config.Comment("Displays coloured boxes over non-empty cubes for debugging purposes.") public static boolean enableChunkStatusDebugging = false; + @Config.LangKey("cubicchunks.config.dump_compute_code") + @Config.Comment("Dumps compute shader code.") + public static boolean dumpComputeShaderCode = false; + @Config.LangKey("cubicchunks.config.relight_checks_per_tick_per_column") @Config.Comment("In an attempt to fix lighting glitches over time, cubic chunks will keep updating light in specified amount of blocks per " + "column (chunk) per tick. This option shouldn't be necessary but may be useful for old worlds where lighting is broken or when " diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/compat/CubicChunksVideoSettings.java b/src/main/java/com/cardinalstar/cubicchunks/api/compat/CubicChunksVideoSettings.java index cd4231a6..6ba1538b 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/api/compat/CubicChunksVideoSettings.java +++ b/src/main/java/com/cardinalstar/cubicchunks/api/compat/CubicChunksVideoSettings.java @@ -1,7 +1,7 @@ package com.cardinalstar.cubicchunks.api.compat; import com.cardinalstar.cubicchunks.CubicChunksConfig; -import com.cardinalstar.cubicchunks.modcompat.angelica.AngelicaInterop; +import com.cardinalstar.cubicchunks.util.Mods; public class CubicChunksVideoSettings { @@ -10,7 +10,7 @@ public static int getMinVerticalViewDistance() { } public static int getMaxVerticalViewDistance() { - return AngelicaInterop.hasDelegate() ? 64 : 32; + return Mods.Angelica.isModLoaded() ? 64 : 32; } public static int getVerticalViewDistance() { diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/AcceleratableWorldGenerator.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/AcceleratableWorldGenerator.java new file mode 100644 index 00000000..a4fd5aa7 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/AcceleratableWorldGenerator.java @@ -0,0 +1,13 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; + +import net.minecraft.world.chunk.Chunk; + +import org.jetbrains.annotations.Nullable; + +import it.unimi.dsi.fastutil.ints.IntArrayList; + +public interface AcceleratableWorldGenerator { + + ComputePlan plan(@Nullable Chunk column, int columnX, int columnZ, IntArrayList cubeY); + +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/ComputePipeline.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/ComputePipeline.java new file mode 100644 index 00000000..0274dca9 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/ComputePipeline.java @@ -0,0 +1,129 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; + +import static org.lwjgl.system.MemoryUtil.memAlloc; +import static org.lwjgl.system.MemoryUtil.memFree; +import static org.lwjgl.vulkan.VK10.VK_PIPELINE_BIND_POINT_COMPUTE; +import static org.lwjgl.vulkan.VK10.VK_SHADER_STAGE_COMPUTE_BIT; +import static org.lwjgl.vulkan.VK10.vkCmdBindPipeline; +import static org.lwjgl.vulkan.VK10.vkCreateComputePipelines; +import static org.lwjgl.vulkan.VK10.vkCreateShaderModule; +import static org.lwjgl.vulkan.VK10.vkDestroyPipeline; +import static org.lwjgl.vulkan.VK10.vkDestroyShaderModule; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.ByteBuffer; +import java.nio.LongBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import net.minecraft.launchwrapper.Launch; + +import org.lwjgl.system.MemoryStack; +import org.lwjgl.vulkan.VkCommandBuffer; +import org.lwjgl.vulkan.VkComputePipelineCreateInfo; +import org.lwjgl.vulkan.VkPipelineShaderStageCreateInfo; +import org.lwjgl.vulkan.VkShaderModuleCreateInfo; + +import com.cardinalstar.cubicchunks.CubicChunksConfig; + +import me.eigenraven.lwjgl3ify.api.Lwjgl3Aware; + +@Lwjgl3Aware +public class ComputePipeline { + + private static Path dumpDir; + + static { + if (CubicChunksConfig.dumpComputeShaderCode) { + int i = 0; + + do { + dumpDir = Launch.minecraftHome.toPath() + .resolve("compute-shader-dumps" + (i++)); + } while (Files.exists(dumpDir)); + + try { + Files.createDirectories(dumpDir); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } + + private long pipeline; + + public ComputePipeline(String executorName, String glslSource) { + try { + Files.write(dumpDir.resolve(executorName + ".glsl"), glslSource.getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + + byte[] spirv = ShaderCache.getOrCompile(glslSource); + + ByteBuffer spirvBuffer = memAlloc(spirv.length); + spirvBuffer.put(spirv) + .flip(); + + try (MemoryStack stack = MemoryStack.stackPush()) { + LongBuffer lb = stack.mallocLong(1); + + VkShaderModuleCreateInfo shaderInfo = VkShaderModuleCreateInfo.calloc(stack) + .sType$Default() + .pCode(spirvBuffer); + + KernelContext.check(vkCreateShaderModule(KernelContext.getDevice(), shaderInfo, null, lb)); + long shaderModule = lb.get(0); + + // spirvBuffer is no longer needed once the shader module is created + memFree(spirvBuffer); + spirvBuffer = null; + + try { + VkComputePipelineCreateInfo.Buffer pipelineInfo = VkComputePipelineCreateInfo.calloc(1, stack); + pipelineInfo.get(0) + .sType$Default() + .layout( + KernelContext.getScheduler() + .getPipelineLayout()) + .stage( + VkPipelineShaderStageCreateInfo.calloc(stack) + .sType$Default() + .stage(VK_SHADER_STAGE_COMPUTE_BIT) + .module(shaderModule) + .pName(stack.ASCII("main"))); + + KernelContext.check( + vkCreateComputePipelines( + KernelContext.getDevice(), + VulkanPipelineCache.getCache(), + pipelineInfo, + null, + lb)); + + pipeline = lb.get(0); + } finally { + vkDestroyShaderModule(KernelContext.getDevice(), shaderModule, null); + } + } finally { + if (spirvBuffer != null) memFree(spirvBuffer); + } + } + + public void bind(VkCommandBuffer cmd) { + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); + } + + public void destroy() { + if (pipeline != 0) { + vkDestroyPipeline(KernelContext.getDevice(), pipeline, null); + pipeline = 0; + } + } + + public long getHandle() { + return pipeline; + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/ComputePlan.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/ComputePlan.java new file mode 100644 index 00000000..fb86fb89 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/ComputePlan.java @@ -0,0 +1,73 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferDataType; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferDescriptor; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferLayout; +import com.google.common.collect.ImmutableMap; + +import it.unimi.dsi.fastutil.ints.Int2ObjectLinkedOpenHashMap; + +public class ComputePlan { + + int nextSubmission, nextBuffer, maxLevel; + + final Int2ObjectLinkedOpenHashMap submits = new Int2ObjectLinkedOpenHashMap<>(); + + final List terminals = new ArrayList<>(); + + public BufferDescriptor describeBuffer(KernelSubmissionToken submission, BufferDataType dataType, int lenX) { + return describeBuffer(submission, dataType, lenX, 1, 1); + } + + public BufferDescriptor describeBuffer(KernelSubmissionToken submission, BufferDataType dataType, int lenX, + int lenY) { + return describeBuffer(submission, dataType, lenX, lenY, 1); + } + + public BufferDescriptor describeBuffer(KernelSubmissionToken submission, BufferDataType dataType, int lenX, + int lenY, int lenZ) { + return new BufferDescriptor(submission, nextBuffer++, dataType, lenX, lenY, lenZ); + } + + public BufferDescriptor describeBuffer(KernelSubmissionToken submission, BufferLayout layout) { + return describeBuffer(submission, layout.dataType(), layout.lenX(), layout.lenY(), layout.lenZ()); + } + + public Map submit(KernelExecutor executor, Key key) { + return submit(executor, key, ImmutableMap.of()); + } + + public Map submit(KernelExecutor executor, Map inputs) { + return submit(executor, null, inputs); + } + + public Map submit(KernelExecutor executor, Key key, + Map inputs) { + int id = nextSubmission++; + + int level = inputs.values() + .stream() + .mapToInt( + d -> d.submission() + .level()) + .max() + .orElse(-1) + 1; + this.maxLevel = Math.max(this.maxLevel, level); + + KernelSubmissionToken submission = new KernelSubmissionToken(executor, key, id, level); + + Map outputs = executor.getOutputs(this, submission, key, inputs); + + submits.put(id, new KernelJob(submission, ImmutableMap.copyOf(inputs), ImmutableMap.copyOf(outputs))); + + return outputs; + } + + public void terminal(Map inputs, TerminalTask task) { + this.terminals.add(new Terminal(inputs, task)); + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelBuilder.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelBuilder.java new file mode 100644 index 00000000..474bc126 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelBuilder.java @@ -0,0 +1,147 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; + +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.util.HashMap; +import java.util.Map; + +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferAccessor; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferDataType; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferLayout; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.ConstantBuffer; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.GPUBuffer; + +public class KernelBuilder { + + private int discriminator = 0; + + public final PushConstantLayout pushConstants = new PushConstantLayout(); + public final ConstantBuffer constants; + + public final StringBuilder preamble = new StringBuilder(); + public final StringBuilder logic = new StringBuilder(); + + public final Map inputs = new HashMap<>(); + public final Map outputs = new HashMap<>(); + + public KernelBuilder(ConstantBuffer constants) { + this.constants = constants; + } + + public String createName(String human) { + return human + "_" + (discriminator++); + } + + public BufferAccessor addConstant(BufferDataType dataType, ByteBuffer data) { + GPUBuffer buffer = constants.addConstant(dataType, data); + + return pushConstants.addConstantOffset(dataType, buffer.getBufferOffset()); + } + + public BufferAccessor addConstant(IntBuffer data) { + GPUBuffer buffer = constants.addConstant(data); + + return pushConstants.addConstantOffset(BufferDataType.i32, buffer.getBufferOffset()); + } + + public BufferAccessor addConstant(int[] data) { + GPUBuffer buffer = constants.addConstant(data); + + return pushConstants.addConstantOffset(BufferDataType.i32, buffer.getBufferOffset()); + } + + public BufferAccessor addConstant(FloatBuffer data) { + GPUBuffer buffer = constants.addConstant(data); + + return pushConstants.addConstantOffset(BufferDataType.f32, buffer.getBufferOffset()); + } + + public BufferAccessor addConstant(float[] data) { + GPUBuffer buffer = constants.addConstant(data); + + return pushConstants.addConstantOffset(BufferDataType.f32, buffer.getBufferOffset()); + } + + public void addBufferMacros(String macroName, BufferAccessor buffer) { + preamble.append("#define GET_") + .append(macroName) + .append("(index) ") + .append( + buffer.getDataType() + .fromUint(buffer.access("index"))) + .append("\n"); + preamble.append("#define SET_") + .append(macroName) + .append("(index, value) ") + .append(buffer.access("index")) + .append(" = ") + .append( + buffer.getDataType() + .toUint("value")) + .append("\n"); + } + + public void addInputBuffer(String name, BufferLayout layout) { + inputs.put(name, layout); + addBufferMacros( + KernelBuilder.toScreamingSnakeCase(name), + pushConstants.addArenaOffset(layout.dataType(), name)); + } + + public void addOutputBuffer(String name, BufferLayout layout) { + outputs.put(name, layout); + addBufferMacros( + KernelBuilder.toScreamingSnakeCase(name), + pushConstants.addArenaOffset(layout.dataType(), name)); + } + + public void addParameter(BufferDataType dataType, String name) { + String pcName = pushConstants.addParameter(dataType, name); + preamble.append("#define GET_") + .append(toScreamingSnakeCase(name)) + .append(" ") + .append(pcName) + .append("\n"); + } + + public void addMacro(String name, String repl) { + preamble.append("#define ") + .append(name) + .append(" ") + .append(repl) + .append("\n"); + } + + public void addMacro(String name, float value) { + preamble.append("#define ") + .append(name) + .append(" ") + .append(value) + .append("f") + .append("\n"); + } + + public void addMacro(String name, int value) { + preamble.append("#define ") + .append(name) + .append(" ") + .append(value) + .append("\n"); + } + + /// theThing -> THE_THING and TheThing -> THE_THING + public static String toScreamingSnakeCase(String camelCase) { + StringBuilder out = new StringBuilder(); + + for (char c : camelCase.toCharArray()) { + if (Character.isUpperCase(c) && out.length() > 0) { + out.append("_"); + } + + out.append(Character.toUpperCase(c)); + } + + return out.toString(); + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelContext.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelContext.java new file mode 100644 index 00000000..9edd7bfb --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelContext.java @@ -0,0 +1,510 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; + +import static org.lwjgl.system.MemoryUtil.memAllocInt; +import static org.lwjgl.system.MemoryUtil.memAllocLong; +import static org.lwjgl.system.MemoryUtil.memAllocPointer; +import static org.lwjgl.util.vma.Vma.vmaCreateAllocator; +import static org.lwjgl.util.vma.Vma.vmaDestroyAllocator; +import static org.lwjgl.vulkan.EXTDebugUtils.VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; +import static org.lwjgl.vulkan.EXTDebugUtils.VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT; +import static org.lwjgl.vulkan.EXTDebugUtils.VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT; +import static org.lwjgl.vulkan.EXTDebugUtils.VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT; +import static org.lwjgl.vulkan.EXTDebugUtils.VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT; +import static org.lwjgl.vulkan.EXTDebugUtils.VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT; +import static org.lwjgl.vulkan.EXTDebugUtils.VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT; +import static org.lwjgl.vulkan.EXTDebugUtils.VK_EXT_DEBUG_UTILS_EXTENSION_NAME; +import static org.lwjgl.vulkan.EXTDebugUtils.vkCreateDebugUtilsMessengerEXT; +import static org.lwjgl.vulkan.KHRGetPhysicalDeviceProperties2.VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME; +import static org.lwjgl.vulkan.VK10.VK_ERROR_EXTENSION_NOT_PRESENT; +import static org.lwjgl.vulkan.VK10.VK_ERROR_INCOMPATIBLE_DRIVER; +import static org.lwjgl.vulkan.VK10.VK_ERROR_OUT_OF_HOST_MEMORY; +import static org.lwjgl.vulkan.VK10.VK_FALSE; +import static org.lwjgl.vulkan.VK10.VK_MEMORY_HEAP_DEVICE_LOCAL_BIT; +import static org.lwjgl.vulkan.VK10.VK_PHYSICAL_DEVICE_TYPE_CPU; +import static org.lwjgl.vulkan.VK10.VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU; +import static org.lwjgl.vulkan.VK10.VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU; +import static org.lwjgl.vulkan.VK10.VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU; +import static org.lwjgl.vulkan.VK10.VK_QUEUE_COMPUTE_BIT; +import static org.lwjgl.vulkan.VK10.VK_QUEUE_GRAPHICS_BIT; +import static org.lwjgl.vulkan.VK10.VK_SUCCESS; +import static org.lwjgl.vulkan.VK10.vkCreateDevice; +import static org.lwjgl.vulkan.VK10.vkCreateInstance; +import static org.lwjgl.vulkan.VK10.vkDestroyDevice; +import static org.lwjgl.vulkan.VK10.vkDestroyInstance; +import static org.lwjgl.vulkan.VK10.vkEnumerateInstanceExtensionProperties; +import static org.lwjgl.vulkan.VK10.vkEnumerateInstanceLayerProperties; +import static org.lwjgl.vulkan.VK10.vkEnumeratePhysicalDevices; +import static org.lwjgl.vulkan.VK10.vkGetDeviceQueue; +import static org.lwjgl.vulkan.VK10.vkGetPhysicalDeviceFeatures; +import static org.lwjgl.vulkan.VK10.vkGetPhysicalDeviceMemoryProperties; +import static org.lwjgl.vulkan.VK10.vkGetPhysicalDeviceProperties; +import static org.lwjgl.vulkan.VK10.vkGetPhysicalDeviceQueueFamilyProperties; +import static org.lwjgl.vulkan.VK12.VK_API_VERSION_1_2; + +import java.io.File; +import java.nio.IntBuffer; +import java.nio.LongBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import net.minecraft.launchwrapper.Launch; + +import org.apache.logging.log4j.Level; +import org.lwjgl.PointerBuffer; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.util.vma.VmaAllocatorCreateInfo; +import org.lwjgl.util.vma.VmaVulkanFunctions; +import org.lwjgl.vulkan.VkApplicationInfo; +import org.lwjgl.vulkan.VkDebugUtilsMessengerCallbackDataEXT; +import org.lwjgl.vulkan.VkDebugUtilsMessengerCallbackEXT; +import org.lwjgl.vulkan.VkDebugUtilsMessengerCreateInfoEXT; +import org.lwjgl.vulkan.VkDevice; +import org.lwjgl.vulkan.VkDeviceCreateInfo; +import org.lwjgl.vulkan.VkDeviceQueueCreateInfo; +import org.lwjgl.vulkan.VkExtensionProperties; +import org.lwjgl.vulkan.VkInstance; +import org.lwjgl.vulkan.VkInstanceCreateInfo; +import org.lwjgl.vulkan.VkLayerProperties; +import org.lwjgl.vulkan.VkMemoryHeap; +import org.lwjgl.vulkan.VkPhysicalDevice; +import org.lwjgl.vulkan.VkPhysicalDeviceFeatures; +import org.lwjgl.vulkan.VkPhysicalDeviceMemoryProperties; +import org.lwjgl.vulkan.VkPhysicalDeviceProperties; +import org.lwjgl.vulkan.VkQueue; +import org.lwjgl.vulkan.VkQueueFamilyProperties; + +import com.cardinalstar.cubicchunks.CubicChunks; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import lombok.Getter; +import me.eigenraven.lwjgl3ify.api.Lwjgl3Aware; + +@Lwjgl3Aware +public class KernelContext { + + private static final boolean ENABLE_VALIDATION = (Boolean) Launch.blackboard.get("fml.deobfuscatedEnvironment") + || Boolean.parseBoolean( + System.getProperty("cubicchunks.compute.validate", "false") + .toLowerCase()); + /// Approximately 1 MB + public static final int CHUNK_SIZE = 1 << 20; + + @Getter + private static boolean enabled; + + private static Thread worker; + + @Getter + private static KernelScheduler scheduler; + + private static final IntBuffer IP = memAllocInt(1); + private static final LongBuffer LP = memAllocLong(1); + private static final PointerBuffer PP = memAllocPointer(1); + + private static VkInstance instance; + private static long messageCallback; + private static VkPhysicalDevice gpu; + @Getter + private static int computeQueueFamily = -1; + private static VkDevice device; + @Getter + private static VkQueue computeQueue; + @Getter + private static long vmaAllocator; + + @SideOnly(Side.CLIENT) + public static void initClient() { + startWorkerThread(); + } + + @SideOnly(Side.SERVER) + public static void initServer() { + startWorkerThread(); + } + + private static void startWorkerThread() { + worker = new Thread(KernelContext::init); + worker.setName("CC-WG-Dispatcher"); + worker.setDaemon(true); + worker.start(); + } + + private static void init() { + createInstance(); + File cacheDir = new File(Launch.minecraftHome, "config/cc-cache"); + ShaderCache.init(cacheDir); + SpirVCompiler.init(); + scanPhysicalDevices(); + selectQueueFamily(); + createLogicalDevice(); + createVmaAllocator(); + VulkanPipelineCache.init(cacheDir); + + CubicChunks.LOGGER.info("Successfully created offscreen compute context"); + + enabled = true; + scheduler = new KernelScheduler(); + + try { + scheduler.run(); + } catch (Throwable t) { + CubicChunks.LOGGER.error("CC-WG-Dispatcher failed", t); + } finally { + try { + scheduler.close(); + } catch (Throwable t) { + CubicChunks.LOGGER.error("Could not clean up KernelScheduler", t); + } + + VulkanPipelineCache.destroy(); + SpirVCompiler.destroy(); + vmaDestroyAllocator(vmaAllocator); + vkDestroyDevice(device, null); + vkDestroyInstance(instance, null); + } + } + + private static final VkDebugUtilsMessengerCallbackEXT dbgFunc = VkDebugUtilsMessengerCallbackEXT + .create((messageSeverity, messageTypes, pCallbackData, pUserData) -> { + Level severity; + if ((messageSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT) != 0) { + severity = Level.TRACE; + } else if ((messageSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT) != 0) { + severity = Level.INFO; + } else if ((messageSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) != 0) { + severity = Level.WARN; + } else if ((messageSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) != 0) { + severity = Level.ERROR; + } else { + severity = Level.DEBUG; + } + + String type; + if ((messageTypes & VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT) != 0) { + type = "GENERAL"; + } else if ((messageTypes & VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT) != 0) { + type = "VALIDATION"; + } else if ((messageTypes & VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT) != 0) { + type = "PERFORMANCE"; + } else { + type = "UNKNOWN"; + } + + VkDebugUtilsMessengerCallbackDataEXT data = VkDebugUtilsMessengerCallbackDataEXT.create(pCallbackData); + + KernelScheduler.LOGGER + .log(severity, "[{}:{}] {}", type, data.pMessageIdNameString(), data.pMessageString()); + + /* + * false indicates that layer should not bail-out of an + * API call that had validation failures. This may mean that the + * app dies inside the driver due to invalid parameter(s). + * That's what would happen without validation layers, so we'll + * keep that behavior here. + */ + return VK_FALSE; + }); + + public static VkDevice getDevice() { + return device; + } + + private static PointerBuffer checkLayers(MemoryStack stack, Set availableLayers, String... requiredLayers) { + for (String req : requiredLayers) { + if (!availableLayers.contains(req)) return null; + } + + PointerBuffer buffer = stack.mallocPointer(requiredLayers.length); + + for (int i = 0; i < requiredLayers.length; i++) { + buffer.put(i, stack.ASCII(requiredLayers[i])); + } + + return buffer; + } + + private static void createInstance() { + try (MemoryStack stack = MemoryStack.stackPush()) { + PointerBuffer ppEnabledLayerNames = null; + + if (ENABLE_VALIDATION) { + check(vkEnumerateInstanceLayerProperties(IP, null)); + + if (IP.get(0) > 0) { + VkLayerProperties.Buffer availableLayers = VkLayerProperties.malloc(IP.get(0), stack); + check(vkEnumerateInstanceLayerProperties(IP, availableLayers)); + + Set layers = availableLayers.stream() + .map(VkLayerProperties::layerNameString) + .collect(Collectors.toSet()); + + // VulkanSDK 1.1.106+ + ppEnabledLayerNames = checkLayers(stack, layers, "VK_LAYER_KHRONOS_validation" + // ,"VK_LAYER_LUNARG_assistant_layer" + ); + if (ppEnabledLayerNames == null) { // use alternative (deprecated) set of validation layers + ppEnabledLayerNames = checkLayers(stack, layers, "VK_LAYER_LUNARG_standard_validation" + // ,"VK_LAYER_LUNARG_assistant_layer" + ); + } + if (ppEnabledLayerNames == null) { // use alternative (deprecated) set of validation layers + ppEnabledLayerNames = checkLayers( + stack, + layers, + "VK_LAYER_GOOGLE_threading", + "VK_LAYER_LUNARG_parameter_validation", + "VK_LAYER_LUNARG_object_tracker", + "VK_LAYER_LUNARG_core_validation", + "VK_LAYER_GOOGLE_unique_objects" + // ,"VK_LAYER_LUNARG_assistant_layer" + ); + } + } + + if (ppEnabledLayerNames == null) { + throw new IllegalStateException( + "vkEnumerateInstanceLayerProperties failed to find required validation layer."); + } + } + + check(vkEnumerateInstanceExtensionProperties((String) null, IP, null)); + + List requiredInstanceExtensions = new ArrayList<>(); + requiredInstanceExtensions.add(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME); + + if (IP.get(0) > 0) { + VkExtensionProperties.Buffer instance_extensions = VkExtensionProperties.malloc(IP.get(0), stack); + check(vkEnumerateInstanceExtensionProperties((String) null, IP, instance_extensions)); + + Set availableExtensions = instance_extensions.stream() + .map(VkExtensionProperties::extensionNameString) + .collect(Collectors.toSet()); + + if (availableExtensions.contains(VK_EXT_DEBUG_UTILS_EXTENSION_NAME) && ENABLE_VALIDATION) { + requiredInstanceExtensions.add(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); + } + } + + PointerBuffer ppEnabledExtensionNames = stack.mallocPointer(requiredInstanceExtensions.size()); + + for (String ext : requiredInstanceExtensions) { + ppEnabledExtensionNames.put(stack.UTF8(ext)); + } + + ppEnabledExtensionNames.flip(); + + VkInstanceCreateInfo pCreateInfo = VkInstanceCreateInfo.calloc(stack) + .sType$Default() + .pApplicationInfo( + VkApplicationInfo.calloc(stack) + .sType$Default() + .pApplicationName(stack.UTF8("Cubic Chunks Compute")) + .apiVersion(VK_API_VERSION_1_2)) + .ppEnabledLayerNames(ppEnabledLayerNames) + .ppEnabledExtensionNames(ppEnabledExtensionNames); + + VkDebugUtilsMessengerCreateInfoEXT dbgCreateInfo = null; + + if (ENABLE_VALIDATION) { + dbgCreateInfo = VkDebugUtilsMessengerCreateInfoEXT.malloc(stack) + .sType$Default() + .messageSeverity( + /* + * VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | + * VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT | + */ + VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) + .messageType( + VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT + | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT) + .pfnUserCallback(dbgFunc); + + pCreateInfo.pNext(dbgCreateInfo); + } + + int err = vkCreateInstance(pCreateInfo, null, PP); + + if (err == VK_ERROR_INCOMPATIBLE_DRIVER) { + throw new IllegalStateException("Cannot find a compatible Vulkan installable client driver (ICD)."); + } else if (err == VK_ERROR_EXTENSION_NOT_PRESENT) { + throw new IllegalStateException( + "Cannot find a specified extension library. Make sure your layers path is set appropriately."); + } else if (err != 0) { + throw new IllegalStateException( + "vkCreateInstance failed. Do you have a compatible Vulkan installable client driver (ICD) installed?"); + } + + instance = new VkInstance(PP.get(0), pCreateInfo); + + if (ENABLE_VALIDATION) { + err = vkCreateDebugUtilsMessengerEXT(instance, dbgCreateInfo, null, LP); + switch (err) { + case VK_SUCCESS: + messageCallback = LP.get(0); + break; + case VK_ERROR_OUT_OF_HOST_MEMORY: + throw new IllegalStateException("CreateDebugReportCallback: out of host memory"); + default: + throw new IllegalStateException("CreateDebugReportCallback: unknown failure"); + } + } + } + } + + private static void scanPhysicalDevices() { + try (MemoryStack stack = MemoryStack.stackPush()) { + /* Make initial call to query gpu_count, then second call for gpu info */ + check(vkEnumeratePhysicalDevices(instance, IP, null)); + + if (IP.get(0) > 0) { + PointerBuffer physical_devices = stack.mallocPointer(IP.get(0)); + check(vkEnumeratePhysicalDevices(instance, IP, physical_devices)); + + VkPhysicalDeviceFeatures features = VkPhysicalDeviceFeatures.calloc(stack); + VkPhysicalDeviceProperties props = VkPhysicalDeviceProperties.calloc(stack); + VkPhysicalDeviceMemoryProperties memProps = VkPhysicalDeviceMemoryProperties.calloc(stack); + + VkPhysicalDevice best = null; + long bestScore = -1; + + for (int i = 0; i < IP.get(0); i++) { + VkPhysicalDevice device = new VkPhysicalDevice(physical_devices.get(i), instance); + + vkGetPhysicalDeviceFeatures(device, features); + vkGetPhysicalDeviceProperties(device, props); + vkGetPhysicalDeviceMemoryProperties(device, memProps); + + long score = scoreDevice(props, memProps); + CubicChunks.LOGGER.info( + "Vulkan device [{}]: type={}, score={}", + props.deviceNameString(), + props.deviceType(), + score); + + if (score > bestScore) { + bestScore = score; + best = device; + } + } + + if (best == null) { + throw new IllegalStateException("No suitable Vulkan physical device found."); + } + + gpu = best; + } else { + throw new IllegalStateException("vkEnumeratePhysicalDevices reported zero accessible devices."); + } + } + } + + private static long scoreDevice(VkPhysicalDeviceProperties props, VkPhysicalDeviceMemoryProperties memProps) { + long score = switch (props.deviceType()) { + case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU -> 4L << 40; + case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU -> 3L << 40; + case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU -> 2L << 40; + case VK_PHYSICAL_DEVICE_TYPE_CPU -> 1L << 40; + default -> 0L; + }; + + for (int h = 0; h < memProps.memoryHeapCount(); h++) { + VkMemoryHeap heap = memProps.memoryHeaps(h); + if ((heap.flags() & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0) { + score += heap.size() >> 20; // MiB of VRAM as tiebreaker + } + } + + return score; + } + + private static void selectQueueFamily() { + try (MemoryStack stack = MemoryStack.stackPush()) { + vkGetPhysicalDeviceQueueFamilyProperties(gpu, IP, null); + int count = IP.get(0); + + if (count == 0) { + throw new IllegalStateException("No Vulkan queue families found."); + } + + VkQueueFamilyProperties.Buffer families = VkQueueFamilyProperties.malloc(count, stack); + vkGetPhysicalDeviceQueueFamilyProperties(gpu, IP, families); + + int dedicatedComputeFamily = -1; + int generalComputeFamily = -1; + + for (int i = 0; i < count; i++) { + int flags = families.get(i) + .queueFlags(); + boolean compute = (flags & VK_QUEUE_COMPUTE_BIT) != 0; + boolean graphics = (flags & VK_QUEUE_GRAPHICS_BIT) != 0; + + if (compute && !graphics && dedicatedComputeFamily == -1) { + dedicatedComputeFamily = i; + } else if (compute && generalComputeFamily == -1) { + generalComputeFamily = i; + } + } + + if (dedicatedComputeFamily != -1) { + computeQueueFamily = dedicatedComputeFamily; + CubicChunks.LOGGER.info("Selected dedicated compute queue family {}", computeQueueFamily); + } else if (generalComputeFamily != -1) { + computeQueueFamily = generalComputeFamily; + CubicChunks.LOGGER.info("Selected general (graphics+compute) queue family {}", computeQueueFamily); + } else { + throw new IllegalStateException("No compute-capable Vulkan queue family found."); + } + } + } + + private static void createLogicalDevice() { + try (MemoryStack stack = MemoryStack.stackPush()) { + VkDeviceQueueCreateInfo.Buffer queueCreateInfos = VkDeviceQueueCreateInfo.calloc(1, stack); + queueCreateInfos.get(0) + .sType$Default() + .queueFamilyIndex(computeQueueFamily) + .pQueuePriorities(stack.floats(1.0f)); + + VkDeviceCreateInfo deviceCreateInfo = VkDeviceCreateInfo.calloc(stack) + .sType$Default() + .pQueueCreateInfos(queueCreateInfos) + .pEnabledFeatures(VkPhysicalDeviceFeatures.calloc(stack)); + + check(vkCreateDevice(gpu, deviceCreateInfo, null, PP)); + device = new VkDevice(PP.get(0), gpu, deviceCreateInfo); + + vkGetDeviceQueue(device, computeQueueFamily, 0, PP); + computeQueue = new VkQueue(PP.get(0), device); + + CubicChunks.LOGGER.info("Logical device and compute queue created"); + } + } + + private static void createVmaAllocator() { + try (MemoryStack stack = MemoryStack.stackPush()) { + VmaVulkanFunctions vkFunctions = VmaVulkanFunctions.calloc(stack) + .set(instance, device); + + VmaAllocatorCreateInfo allocatorCreateInfo = VmaAllocatorCreateInfo.calloc(stack) + .physicalDevice(gpu) + .device(device) + .instance(instance) + .vulkanApiVersion(VK_API_VERSION_1_2) + .pVulkanFunctions(vkFunctions); + + check(vmaCreateAllocator(allocatorCreateInfo, PP)); + vmaAllocator = PP.get(0); + + CubicChunks.LOGGER.info("VMA allocator created"); + } + } + + static void check(int errcode) { + if (errcode != 0) { + throw new IllegalStateException(String.format("Vulkan error [0x%X]", errcode)); + } + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelExecutor.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelExecutor.java new file mode 100644 index 00000000..3640ad4d --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelExecutor.java @@ -0,0 +1,36 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; + +import java.io.Closeable; +import java.util.Map; + +import org.lwjgl.vulkan.VkCommandBuffer; + +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferAllocator; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferDescriptor; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.ConstantBuffer; +import com.cardinalstar.cubicchunks.async.CallingThread; +import com.cardinalstar.cubicchunks.async.ThreadType; + +import me.eigenraven.lwjgl3ify.api.Lwjgl3Aware; + +@Lwjgl3Aware +public interface KernelExecutor extends Closeable { + + @CallingThread(ThreadType.WORKER) + @Override + void close(); + + @CallingThread(ThreadType.WORKER) + boolean isCompiled(); + + @CallingThread(ThreadType.WORKER) + void compile(ConstantBuffer constants); + + @CallingThread(ThreadType.SERVER) + Map getOutputs(ComputePlan plan, KernelSubmissionToken submission, Key key, + Map inputs); + + @CallingThread(ThreadType.WORKER) + KernelSubmissionResult[] submit(VkCommandBuffer commands, BufferAllocator alloc, + KernelSubmission[] submissions); +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelJob.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelJob.java new file mode 100644 index 00000000..e647748c --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelJob.java @@ -0,0 +1,12 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; + +import java.util.Map; + +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferDescriptor; +import com.github.bsideup.jabel.Desugar; + +@Desugar +record KernelJob(KernelSubmissionToken submission, Map inputs, + Map outputs) { + +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelScheduler.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelScheduler.java new file mode 100644 index 00000000..843bff8d --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelScheduler.java @@ -0,0 +1,907 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; + +import static com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelContext.check; +import static com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelContext.getDevice; +import static org.lwjgl.vulkan.VK10.VK_ACCESS_SHADER_READ_BIT; +import static org.lwjgl.vulkan.VK10.VK_ACCESS_SHADER_WRITE_BIT; +import static org.lwjgl.vulkan.VK10.VK_ACCESS_TRANSFER_READ_BIT; +import static org.lwjgl.vulkan.VK10.VK_ACCESS_TRANSFER_WRITE_BIT; +import static org.lwjgl.vulkan.VK10.VK_COMMAND_BUFFER_LEVEL_PRIMARY; +import static org.lwjgl.vulkan.VK10.VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; +import static org.lwjgl.vulkan.VK10.VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; +import static org.lwjgl.vulkan.VK10.VK_COMMAND_POOL_CREATE_TRANSIENT_BIT; +import static org.lwjgl.vulkan.VK10.VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; +import static org.lwjgl.vulkan.VK10.VK_FENCE_CREATE_SIGNALED_BIT; +import static org.lwjgl.vulkan.VK10.VK_PIPELINE_BIND_POINT_COMPUTE; +import static org.lwjgl.vulkan.VK10.VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT; +import static org.lwjgl.vulkan.VK10.VK_PIPELINE_STAGE_TRANSFER_BIT; +import static org.lwjgl.vulkan.VK10.VK_QUEUE_FAMILY_IGNORED; +import static org.lwjgl.vulkan.VK10.VK_SHADER_STAGE_COMPUTE_BIT; +import static org.lwjgl.vulkan.VK10.vkAllocateCommandBuffers; +import static org.lwjgl.vulkan.VK10.vkAllocateDescriptorSets; +import static org.lwjgl.vulkan.VK10.vkBeginCommandBuffer; +import static org.lwjgl.vulkan.VK10.vkCmdBindDescriptorSets; +import static org.lwjgl.vulkan.VK10.vkCmdCopyBuffer; +import static org.lwjgl.vulkan.VK10.vkCmdFillBuffer; +import static org.lwjgl.vulkan.VK10.vkCmdPipelineBarrier; +import static org.lwjgl.vulkan.VK10.vkCreateCommandPool; +import static org.lwjgl.vulkan.VK10.vkCreateDescriptorPool; +import static org.lwjgl.vulkan.VK10.vkCreateDescriptorSetLayout; +import static org.lwjgl.vulkan.VK10.vkCreateFence; +import static org.lwjgl.vulkan.VK10.vkCreatePipelineLayout; +import static org.lwjgl.vulkan.VK10.vkDestroyCommandPool; +import static org.lwjgl.vulkan.VK10.vkDestroyDescriptorPool; +import static org.lwjgl.vulkan.VK10.vkDestroyDescriptorSetLayout; +import static org.lwjgl.vulkan.VK10.vkDestroyFence; +import static org.lwjgl.vulkan.VK10.vkDestroyPipelineLayout; +import static org.lwjgl.vulkan.VK10.vkEndCommandBuffer; +import static org.lwjgl.vulkan.VK10.vkFreeCommandBuffers; +import static org.lwjgl.vulkan.VK10.vkFreeDescriptorSets; +import static org.lwjgl.vulkan.VK10.vkQueueSubmit; +import static org.lwjgl.vulkan.VK10.vkResetCommandBuffer; +import static org.lwjgl.vulkan.VK10.vkResetFences; +import static org.lwjgl.vulkan.VK10.vkUpdateDescriptorSets; +import static org.lwjgl.vulkan.VK10.vkWaitForFences; + +import java.io.Closeable; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.IntBuffer; +import java.nio.LongBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.lwjgl.PointerBuffer; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.system.MemoryUtil; +import org.lwjgl.vulkan.VkBufferCopy; +import org.lwjgl.vulkan.VkBufferMemoryBarrier; +import org.lwjgl.vulkan.VkCommandBuffer; +import org.lwjgl.vulkan.VkCommandBufferAllocateInfo; +import org.lwjgl.vulkan.VkCommandBufferBeginInfo; +import org.lwjgl.vulkan.VkCommandPoolCreateInfo; +import org.lwjgl.vulkan.VkDescriptorBufferInfo; +import org.lwjgl.vulkan.VkDescriptorPoolCreateInfo; +import org.lwjgl.vulkan.VkDescriptorPoolSize; +import org.lwjgl.vulkan.VkDescriptorSetAllocateInfo; +import org.lwjgl.vulkan.VkDescriptorSetLayoutBinding; +import org.lwjgl.vulkan.VkDescriptorSetLayoutCreateInfo; +import org.lwjgl.vulkan.VkDevice; +import org.lwjgl.vulkan.VkFenceCreateInfo; +import org.lwjgl.vulkan.VkPipelineLayoutCreateInfo; +import org.lwjgl.vulkan.VkPushConstantRange; +import org.lwjgl.vulkan.VkSubmitInfo; +import org.lwjgl.vulkan.VkWriteDescriptorSet; + +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferDescriptor; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.GPUBuffer; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.VulkanArenaAllocator; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.VulkanConstantPool; +import com.cardinalstar.cubicchunks.async.CallingThread; +import com.cardinalstar.cubicchunks.async.ThreadType; +import com.cardinalstar.cubicchunks.util.DataUtils; +import com.cardinalstar.cubicchunks.util.JavaUtils; +import com.cardinalstar.cubicchunks.util.MathUtil; +import com.github.bsideup.jabel.Desugar; + +import it.unimi.dsi.fastutil.Pair; +import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap.Entry; +import it.unimi.dsi.fastutil.objects.Object2IntMaps; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import lombok.Getter; +import me.eigenraven.lwjgl3ify.api.Lwjgl3Aware; + +@Lwjgl3Aware +@SuppressWarnings("rawtypes") +public class KernelScheduler implements Closeable { + + @Desugar + private record TerminalResult(Map data, TerminalTask task) {} + + @Desugar + private record ArenaRange(long offset, long byteLen) {} + + @Desugar + private record CopyRegion(long srcOffset, long dstOffset, long byteLen) {} + + // Carries the plan-local-to-global index offsets alongside the job. + @Desugar + private record GlobalJob(int planBase, int bufferBase, KernelJob job) {} + + private interface Task { + } + + @Desugar + private record SubmitTask(List plans, AtomicBoolean ready) implements Task {} + + @Desugar + private record ExecTask(Runnable fn, AtomicBoolean done) implements Task {} + + public static final Logger LOGGER = LogManager.getLogger("CC-KernelScheduler"); + + // EMA parameters for per-executor timing estimates. + private static final double EMA_ALPHA = 0.1; + // Initial per-kernel estimate used before any measurements are available. + private static final long DEFAULT_ESTIMATE_NS = 100_000L; // 0.1 ms + + private final LinkedBlockingQueue tasks = new LinkedBlockingQueue<>(); + private final LinkedBlockingQueue results = new LinkedBlockingQueue<>(); + + // Per-executor EMA: long[0] = ema nanoseconds per dispatch, long[1] = sample count. + private final Map executorEma = new IdentityHashMap<>(); + private final Object emaLock = new Object(); + + private long gpuBudgetNs = 20_000_000L; // 20 ms default, configurable + + private final LongBuffer lp; + private final PointerBuffer pp; + private final IntBuffer ip; + + private long descriptorSetLayout0, descriptorSetLayout1, descriptorSetLayout2; + + @Getter + private long pipelineLayout; + + private long descriptorPool; + private long descriptorSet0, descriptorSet1, descriptorSet2; + + private long commandPool; + @Getter + private VkCommandBuffer commandBuffer; + + private long fence; + + private final VulkanArenaAllocator arenaAllocator = new VulkanArenaAllocator(); + + @Getter + private VulkanBuffer arena, readback; + @Getter + private final VulkanConstantPool constants; + + private final Set> knownExecutors = new HashSet<>(); + + public KernelScheduler() { + lp = MemoryUtil.memAllocLong(1); + pp = MemoryUtil.memAllocPointer(1); + ip = MemoryUtil.memAllocInt(1); + + createDescriptorSetLayouts(); + createPipelineLayout(); + + createDescriptorPool(); + allocateDescriptorSets(); + + createCommandPool(); + createFence(); + + constants = new VulkanConstantPool(KernelContext.getVmaAllocator()); + arena = VulkanBuffer.allocDeviceLocal(KernelContext.getVmaAllocator(), KernelContext.CHUNK_SIZE); + readback = VulkanBuffer.allocHostVisible(KernelContext.getVmaAllocator(), KernelContext.CHUNK_SIZE); + } + + @Override + public void close() { + VkDevice device = getDevice(); + + knownExecutors.forEach(KernelExecutor::close); + + if (fence != 0) vkDestroyFence(device, fence, null); + + if (commandBuffer != null) vkFreeCommandBuffers(device, commandPool, commandBuffer); + if (commandPool != 0) vkDestroyCommandPool(device, commandPool, null); + + if (descriptorSet0 != 0) vkFreeDescriptorSets(device, descriptorPool, descriptorSet0); + if (descriptorSet1 != 0) vkFreeDescriptorSets(device, descriptorPool, descriptorSet1); + if (descriptorSet2 != 0) vkFreeDescriptorSets(device, descriptorPool, descriptorSet2); + if (descriptorPool != 0) vkDestroyDescriptorPool(device, descriptorPool, null); + + if (pipelineLayout != 0) vkDestroyPipelineLayout(device, pipelineLayout, null); + + if (descriptorSetLayout0 != 0) vkDestroyDescriptorSetLayout(device, descriptorSetLayout0, null); + if (descriptorSetLayout1 != 0) vkDestroyDescriptorSetLayout(device, descriptorSetLayout1, null); + if (descriptorSetLayout2 != 0) vkDestroyDescriptorSetLayout(device, descriptorSetLayout2, null); + + MemoryUtil.memFree(lp); + MemoryUtil.memFree(pp); + MemoryUtil.memFree(ip); + } + + // ------------------------------------------------------------------------- + // Public API + // ------------------------------------------------------------------------- + + @CallingThread(ThreadType.SERVER) + public void setGpuBudget(long ns) { + this.gpuBudgetNs = ns; + } + + @CallingThread(ThreadType.SERVER) + public long getGpuBudget() { + return gpuBudgetNs; + } + + /** + * Estimates the GPU wall-clock cost of executing all kernels in {@code plan} based on + * the exponential moving average of observed dispatch times per executor type. + * Returns a conservative over-estimate before any measurements are available. + */ + @CallingThread(ThreadType.SERVER) + public long estimatePlanCost(ComputePlan plan) { + synchronized (emaLock) { + long total = 0; + for (var e : plan.submits.int2ObjectEntrySet()) { + total += getEstimateNs( + e.getValue() + .submission() + .executor()); + } + return total; + } + } + + @CallingThread(ThreadType.SERVER) + public void submit(List plans) { + if (plans.isEmpty()) return; + + AtomicBoolean ready = new AtomicBoolean(false); + tasks.add(new SubmitTask(plans, ready)); + + while (!ready.get()) { + processResults(); + + Thread.yield(); + JavaUtils.onSpinWait(); + } + + // Drain any results added between the last poll and ready being set. + processResults(); + } + + @CallingThread(ThreadType.SERVER) + public void runAndWait(Runnable fn) { + AtomicBoolean done = new AtomicBoolean(false); + tasks.add(new ExecTask(fn, done)); + while (!done.get()) { + Thread.yield(); + JavaUtils.onSpinWait(); + } + } + + @CallingThread(ThreadType.SERVER) + public void processResults() { + TerminalResult result; + + while ((result = results.poll()) != null) { + result.task() + .execute(result.data()); + } + } + + @CallingThread(ThreadType.SERVER) + public void compileExecutor(KernelExecutor executor) { + runAndWait(() -> { executor.compile(this.constants); }); + } + + @CallingThread(ThreadType.CLIENT) + public void run() { + while (true) { + Task task; + + try { + task = tasks.take(); + } catch (InterruptedException e) { + continue; + } + + if (task instanceof SubmitTask submit) { + this.submitPlans(submit.plans, submit.ready); + } else if (task instanceof ExecTask exec) { + exec.fn() + .run(); + exec.done() + .set(true); + } + } + } + + @CallingThread(ThreadType.CLIENT) + private void submitPlans(List plans, AtomicBoolean ready) { + LOGGER.info("Processing batch with {} plans", plans.size()); + + int totalJobs = 0; + int totalBuffers = 0; + int[] planBase = new int[plans.size()]; + int[] bufferBase = new int[plans.size()]; + + for (int p = 0; p < plans.size(); p++) { + planBase[p] = totalJobs; + bufferBase[p] = totalBuffers; + totalJobs += plans.get(p).nextSubmission; + totalBuffers += plans.get(p).nextBuffer; + } + + GlobalJob[] globalJobs = new GlobalJob[totalJobs]; + + for (int p = 0; p < plans.size(); p++) { + for (Entry e : plans.get(p).submits.int2ObjectEntrySet()) { + globalJobs[planBase[p] + e.getIntKey()] = new GlobalJob(planBase[p], bufferBase[p], e.getValue()); + } + } + + int maxLevel = totalJobs > 0 ? plans.stream() + .mapToInt(p -> p.maxLevel) + .max() + .getAsInt() : 0; + + int arenaLength = 0; + + for (GlobalJob job : globalJobs) { + for (var output : job.job.outputs() + .values()) { + arenaLength += MathUtil.alignTo(output.getBufferLength(), 16); + } + } + + arenaLength *= 2; + + GPUBuffer[] buffers = new GPUBuffer[totalBuffers]; + + // Track how many real (non-cached) dispatches each executor made, for EMA timing. + Object2IntOpenHashMap dispatchCounts = new Object2IntOpenHashMap<>(); + + // Pre-compute readback capacity from terminal descriptors so we can resize + // before recording vkCmdFillBuffer, avoiding use of a destroyed VkBuffer. + int neededReadback = 0; + { + Set seen = new HashSet<>(); + for (int p = 0; p < plans.size(); p++) { + int bb = bufferBase[p]; + for (var terminal : plans.get(p).terminals) { + for (BufferDescriptor desc : terminal.inputs() + .values()) { + int gid = bb + desc.bufferId(); + if (seen.add(gid)) { + neededReadback += desc.getBufferLength(); + } + } + } + } + } + if (readback.byteLen() < neededReadback) { + readback = readback + .resize(KernelContext.getVmaAllocator(), MathUtil.alignTo(neededReadback, KernelContext.CHUNK_SIZE)); + } + + long batchStart = System.nanoTime(); + + beginVulkanBatch(); + + constants.update(commandBuffer); + + arenaAllocator.reset(); + + if (arena.byteLen() < arenaLength) { + arena = arena + .resize(KernelContext.getVmaAllocator(), MathUtil.alignTo(arenaLength, KernelContext.CHUNK_SIZE)); + } + + bindDescriptorBuffers(); + + vkCmdFillBuffer(commandBuffer, arena.buffer(), 0, arena.byteLen(), 0); + vkCmdFillBuffer(commandBuffer, readback.buffer(), 0, readback.byteLen(), 0); + + try (MemoryStack stack = MemoryStack.stackPush()) { + VkBufferMemoryBarrier.Buffer barriers = VkBufferMemoryBarrier.calloc(2, stack); + + barriers.get(0) + .sType$Default() + .srcAccessMask(VK_ACCESS_TRANSFER_WRITE_BIT) + .dstAccessMask(VK_ACCESS_SHADER_READ_BIT) + .srcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) + .dstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) + .buffer(arena.buffer()) + .offset(0) + .size(arena.byteLen()); + + barriers.get(1) + .sType$Default() + .srcAccessMask(VK_ACCESS_TRANSFER_WRITE_BIT) + .dstAccessMask(VK_ACCESS_SHADER_READ_BIT) + .srcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) + .dstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) + .buffer(readback.buffer()) + .offset(0) + .size(readback.byteLen()); + + vkCmdPipelineBarrier( + commandBuffer, + VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + 0, + null, + barriers, + null); + } + + for (int l = 0; l <= maxLevel; l++) { + Map>> submits = new HashMap<>(); + + for (int i = 0; i < totalJobs; i++) { + KernelJob job = globalJobs[i].job(); + KernelSubmissionToken submission = job.submission(); + + if (submission.level() == l) { + var s = submits.computeIfAbsent(submission.executor(), $ -> new ArrayList<>()); + + Map inputs = new HashMap<>(); + + // Resolve input buffers using the plan's buffer-base offset. + // All dependency jobs are guaranteed submitted (level ordering ensures this). + for (Map.Entry e : job.inputs() + .entrySet()) { + inputs.put( + e.getKey(), + buffers[globalJobs[i].bufferBase() + e.getValue() + .bufferId()]); + } + + // noinspection unchecked + s.add(Pair.of(globalJobs[i], new KernelSubmission(submission.key(), inputs))); + + dispatchCounts.addTo(submission.executor(), 1); + } + } + + submits.forEach((exec, submissionList) -> { + try { + KernelSubmission[] submissionArray = DataUtils + .mapToArray(submissionList, KernelSubmission[]::new, Pair::right); + + @SuppressWarnings("unchecked") + KernelSubmissionResult[] results = exec.submit(commandBuffer, arenaAllocator, submissionArray); + + List dispatchOutputRanges = new ArrayList<>(); + + for (int i = 0; i < results.length; i++) { + var pair = submissionList.get(i); + + // Write output buffers into the global buffer array using the plan's buffer-base offset. + for (Map.Entry e : results[i].outputs() + .entrySet()) { + BufferDescriptor outDesc = pair.left() + .job() + .outputs() + .get(e.getKey()); + GPUBuffer outBuf = e.getValue(); + buffers[pair.left() + .bufferBase() + outDesc.bufferId()] = outBuf; + dispatchOutputRanges + .add(new ArenaRange(outBuf.getBufferOffset(), outBuf.getBufferLength())); + } + } + + insertOutputBarriers(commandBuffer, dispatchOutputRanges); + } catch (Throwable t) { + LOGGER.error( + "Error while submitting compute dispatches for kernel executor {}.\nKeys: {}", + exec, + submissionList, + t); + throw new RuntimeException( + "Error while submitting compute dispatched for kernel executor " + exec, + t); + } + }); + } + + // --- Step D: identify terminal outputs and build CopyRegion list --- + // + // Collect the set of global buffer indices referenced by any terminal across all plans. + // Each unique terminal buffer gets a slot in the readback buffer. + + // Map from global buffer index to readback byte offset. + Int2IntOpenHashMap terminalReadbackOffsets = new Int2IntOpenHashMap(); + int readbackOffset = 0; + + for (int p = 0; p < plans.size(); p++) { + ComputePlan plan = plans.get(p); + int bb = bufferBase[p]; + + for (var terminal : plan.terminals) { + for (BufferDescriptor desc : terminal.inputs() + .values()) { + int globalBufIdx = bb + desc.bufferId(); + if (!terminalReadbackOffsets.containsKey(globalBufIdx)) { + terminalReadbackOffsets.put(globalBufIdx, readbackOffset); + GPUBuffer buf = buffers[globalBufIdx]; + readbackOffset += buf.getBufferLength(); + } + } + } + } + + List copies = new ArrayList<>(); + + terminalReadbackOffsets.forEach((int bufId, int offset) -> { + GPUBuffer buf = buffers[bufId]; + copies.add(new CopyRegion(buf.getBufferOffset(), offset, buf.getBufferLength())); + }); + + submitVulkanBatch(commandBuffer, copies); + + // GPU is now idle; read terminal data from the persistently mapped readback buffer. + long batchElapsed = System.nanoTime() - batchStart; + updateEMAs(dispatchCounts, batchElapsed); + + LOGGER.info("Batch took {} ms", String.format("%.2f", batchElapsed / 1000000f)); + + // Buffer descriptors in terminal inputs use local buffer IDs; offset by bufferBase[p]. + for (int p = 0; p < plans.size(); p++) { + ComputePlan plan = plans.get(p); + int bb = bufferBase[p]; + + // Cache downloads within a plan: multiple terminals may share the same buffer. + ByteBuffer[] downloads = new ByteBuffer[plan.nextBuffer]; + + for (var terminal : plan.terminals) { + Map inputs = resolveReadbackDownloads( + terminal.inputs(), + bb, + buffers, + downloads, + terminalReadbackOffsets, + readback.mapped()); + this.results.add(new TerminalResult(inputs, terminal.task())); + } + } + + ready.set(true); + } + + // ------------------------------------------------------------------------- + // Vulkan dispatch helpers + // ------------------------------------------------------------------------- + + @CallingThread(ThreadType.CLIENT) + private void beginVulkanBatch() { + try (MemoryStack stack = MemoryStack.stackPush()) { + check(vkResetCommandBuffer(commandBuffer, 0)); + check( + vkBeginCommandBuffer( + commandBuffer, + VkCommandBufferBeginInfo.calloc(stack) + .sType$Default() + .flags(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT))); + vkCmdBindDescriptorSets( + commandBuffer, + VK_PIPELINE_BIND_POINT_COMPUTE, + pipelineLayout, + 0, + stack.longs(descriptorSet0, descriptorSet1), + null); + } + } + + @CallingThread(ThreadType.CLIENT) + private void insertOutputBarriers(VkCommandBuffer cmd, List outputs) { + if (outputs.isEmpty()) return; + + long arenaBuf = arena.buffer(); + + VkBufferMemoryBarrier.Buffer barriers = VkBufferMemoryBarrier.calloc(outputs.size()); + + try { + for (int i = 0; i < outputs.size(); i++) { + ArenaRange r = outputs.get(i); + barriers.get(i) + .sType$Default() + .srcAccessMask(VK_ACCESS_SHADER_WRITE_BIT) + .dstAccessMask(VK_ACCESS_SHADER_READ_BIT) + .srcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) + .dstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) + .buffer(arenaBuf) + .offset(r.offset()) + .size(r.byteLen()); + } + + vkCmdPipelineBarrier( + cmd, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + 0, + null, + barriers, + null); + } finally { + barriers.free(); + } + } + + @CallingThread(ThreadType.WORKER) + private void submitVulkanBatch(VkCommandBuffer cmd, List toCopy) { + try (MemoryStack stack = MemoryStack.stackPush()) { + // COMPUTE → TRANSFER: scoped to terminal output regions only + long arenaBuf = arena.buffer(); + long readbackBuf = readback.buffer(); + + if (!toCopy.isEmpty()) { + VkBufferMemoryBarrier.Buffer copyBarriers = VkBufferMemoryBarrier.calloc(toCopy.size()); + + try { + for (int i = 0; i < toCopy.size(); i++) { + CopyRegion r = toCopy.get(i); + copyBarriers.get(i) + .sType$Default() + .srcAccessMask(VK_ACCESS_SHADER_WRITE_BIT) + .dstAccessMask(VK_ACCESS_TRANSFER_READ_BIT) + .srcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) + .dstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) + .buffer(arenaBuf) + .offset(r.srcOffset()) + .size(r.byteLen()); + } + + vkCmdPipelineBarrier( + cmd, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, + 0, + null, + copyBarriers, + null); + } finally { + copyBarriers.free(); + } + + var copyRegions = VkBufferCopy.calloc(toCopy.size()); + + try { + for (int i = 0; i < toCopy.size(); i++) { + KernelScheduler.CopyRegion r = toCopy.get(i); + copyRegions.get(i) + .srcOffset(r.srcOffset()) + .dstOffset(r.dstOffset()) + .size(r.byteLen()); + } + + vkCmdCopyBuffer(cmd, arenaBuf, readbackBuf, copyRegions); + } finally { + copyRegions.free(); + } + } + + check(vkEndCommandBuffer(cmd)); + + check(vkResetFences(getDevice(), stack.longs(fence))); + check( + vkQueueSubmit( + KernelContext.getComputeQueue(), + VkSubmitInfo.calloc(stack) + .sType$Default() + .pCommandBuffers(stack.pointers(cmd)), + fence)); + check(vkWaitForFences(getDevice(), stack.longs(fence), true, Long.MAX_VALUE)); + } + } + + /// Reads terminal input buffers from the Vulkan readback buffer's mapped view. + /// Each buffer that has not yet been sliced is extracted from the readback mapped ByteBuffer + /// using the pre-computed readback offset tracked during CopyRegion construction. + @CallingThread(ThreadType.CLIENT) + private Map resolveReadbackDownloads(Map terminalInputs, + int bufferBaseOffset, GPUBuffer[] buffers, ByteBuffer[] downloads, Int2IntOpenHashMap terminalReadbackOffsets, + ByteBuffer readbackMapped) { + Map out = new HashMap<>(); + + for (var input : terminalInputs.entrySet()) { + int localBufId = input.getValue() + .bufferId(); + + if (downloads[localBufId] == null) { + int globalBufIdx = bufferBaseOffset + localBufId; + GPUBuffer buf = buffers[globalBufIdx]; + int byteLen = buf.getBufferLength(); + int rbOffset = terminalReadbackOffsets.get(globalBufIdx); + + ByteBuffer slice = ByteBuffer.allocateDirect(byteLen) + .order(ByteOrder.nativeOrder()); + + MemoryUtil + .memCopy(MemoryUtil.memAddress(readbackMapped) + rbOffset, MemoryUtil.memAddress(slice), byteLen); + + downloads[localBufId] = slice; + } + + out.put(input.getKey(), downloads[localBufId]); + } + + return out; + } + + // ------------------------------------------------------------------------- + // EMA timing + // ------------------------------------------------------------------------- + + @CallingThread(ThreadType.CLIENT) + private void updateEMAs(Object2IntOpenHashMap dispatchCounts, long totalElapsedNs) { + int totalDispatches = dispatchCounts.values() + .intStream() + .sum(); + + if (totalDispatches == 0) return; + // Attribute wall-clock time equally across all dispatched kernels. + // This is approximate (kernels overlap on the GPU), but sufficient for budgeting. + long nsPerDispatch = totalElapsedNs / totalDispatches; + + synchronized (emaLock) { + for (var e : Object2IntMaps.fastIterable(dispatchCounts)) { + recordSample(e.getKey(), nsPerDispatch); + } + } + } + + @CallingThread(ThreadType.SERVER) + private long getEstimateNs(KernelExecutor exec) { + long[] ema = executorEma.get(exec); + return (ema == null || ema[1] == 0) ? DEFAULT_ESTIMATE_NS : ema[0]; + } + + @CallingThread(ThreadType.CLIENT) + private void recordSample(KernelExecutor exec, long sampleNs) { + long[] ema = executorEma.computeIfAbsent(exec, k -> new long[2]); + ema[0] = ema[1] == 0 ? sampleNs : (long) (EMA_ALPHA * sampleNs + (1 - EMA_ALPHA) * ema[0]); + ema[1]++; + } + + private void createDescriptorSetLayouts() { + try (MemoryStack stack = MemoryStack.stackPush()) { + VkDescriptorSetLayoutBinding.Buffer binding = VkDescriptorSetLayoutBinding.calloc(1, stack) + .binding(0) + .descriptorType(VK_DESCRIPTOR_TYPE_STORAGE_BUFFER) + .descriptorCount(1) + .stageFlags(VK_SHADER_STAGE_COMPUTE_BIT); + + VkDescriptorSetLayoutCreateInfo layoutInfo = VkDescriptorSetLayoutCreateInfo.calloc(stack) + .sType$Default() + .pBindings(binding); + + check(vkCreateDescriptorSetLayout(getDevice(), layoutInfo, null, lp)); + descriptorSetLayout0 = lp.get(0); + + check(vkCreateDescriptorSetLayout(getDevice(), layoutInfo, null, lp)); + descriptorSetLayout1 = lp.get(0); + + check(vkCreateDescriptorSetLayout(getDevice(), layoutInfo, null, lp)); + descriptorSetLayout2 = lp.get(0); + } + } + + private void createPipelineLayout() { + try (MemoryStack stack = MemoryStack.stackPush()) { + VkPushConstantRange.Buffer pushConstantRange = VkPushConstantRange.calloc(1, stack) + .stageFlags(VK_SHADER_STAGE_COMPUTE_BIT) + .offset(0) + .size(128); + + VkPipelineLayoutCreateInfo layoutInfo = VkPipelineLayoutCreateInfo.calloc(stack) + .sType$Default() + .pSetLayouts(stack.longs(descriptorSetLayout0, descriptorSetLayout1, descriptorSetLayout2)) + .pPushConstantRanges(pushConstantRange); + + check(vkCreatePipelineLayout(getDevice(), layoutInfo, null, lp)); + pipelineLayout = lp.get(0); + } + } + + private void createDescriptorPool() { + try (MemoryStack stack = MemoryStack.stackPush()) { + // Three descriptor sets (set 0 = constants, set 1 = arena, set 2 = dynamic custom), each with one SSBO + // binding. + VkDescriptorPoolSize.Buffer poolSize = VkDescriptorPoolSize.calloc(1, stack) + .type(VK_DESCRIPTOR_TYPE_STORAGE_BUFFER) + .descriptorCount(3); + + VkDescriptorPoolCreateInfo poolInfo = VkDescriptorPoolCreateInfo.calloc(stack) + .sType$Default() + .maxSets(3) + .pPoolSizes(poolSize); + + check(vkCreateDescriptorPool(getDevice(), poolInfo, null, lp)); + descriptorPool = lp.get(0); + } + } + + private void allocateDescriptorSets() { + try (MemoryStack stack = MemoryStack.stackPush()) { + VkDescriptorSetAllocateInfo allocInfo = VkDescriptorSetAllocateInfo.calloc(stack) + .sType$Default() + .descriptorPool(descriptorPool) + .pSetLayouts(stack.longs(descriptorSetLayout0, descriptorSetLayout1, descriptorSetLayout2)); + + LongBuffer pSets = stack.mallocLong(3); + check(vkAllocateDescriptorSets(getDevice(), allocInfo, pSets)); + descriptorSet0 = pSets.get(0); + descriptorSet1 = pSets.get(1); + descriptorSet2 = pSets.get(2); + } + } + + private void bindDescriptorBuffers() { + try (MemoryStack stack = MemoryStack.stackPush()) { + VkDescriptorBufferInfo.Buffer bufInfoConstants = VkDescriptorBufferInfo.calloc(1, stack) + .buffer( + constants.getDeviceBuffer() + .buffer()) + .offset(0) + .range( + constants.getDeviceBuffer() + .byteLen()); + + VkDescriptorBufferInfo.Buffer bufInfoArena = VkDescriptorBufferInfo.calloc(1, stack) + .buffer(arena.buffer()) + .offset(0) + .range(arena.byteLen()); + + VkWriteDescriptorSet.Buffer writes = VkWriteDescriptorSet.calloc(2, stack); + + writes.get(0) + .sType$Default() + .dstSet(descriptorSet0) + .dstBinding(0) + .descriptorCount(1) + .descriptorType(VK_DESCRIPTOR_TYPE_STORAGE_BUFFER) + .pBufferInfo(bufInfoConstants); + + writes.get(1) + .sType$Default() + .dstSet(descriptorSet1) + .dstBinding(0) + .descriptorCount(1) + .descriptorType(VK_DESCRIPTOR_TYPE_STORAGE_BUFFER) + .pBufferInfo(bufInfoArena); + + vkUpdateDescriptorSets(getDevice(), writes, null); + } + } + + private void createCommandPool() { + try (MemoryStack stack = MemoryStack.stackPush()) { + VkCommandPoolCreateInfo poolInfo = VkCommandPoolCreateInfo.calloc(stack) + .sType$Default() + .flags(VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT | VK_COMMAND_POOL_CREATE_TRANSIENT_BIT) + .queueFamilyIndex(KernelContext.getComputeQueueFamily()); + + check(vkCreateCommandPool(getDevice(), poolInfo, null, lp)); + commandPool = lp.get(0); + + VkCommandBufferAllocateInfo allocInfo = VkCommandBufferAllocateInfo.calloc(stack) + .sType$Default() + .commandPool(commandPool) + .level(VK_COMMAND_BUFFER_LEVEL_PRIMARY) + .commandBufferCount(1); + + check(vkAllocateCommandBuffers(getDevice(), allocInfo, pp)); + commandBuffer = new VkCommandBuffer(pp.get(0), getDevice()); + } + } + + private void createFence() { + try (MemoryStack stack = MemoryStack.stackPush()) { + VkFenceCreateInfo fenceInfo = VkFenceCreateInfo.calloc(stack) + .sType$Default() + .flags(VK_FENCE_CREATE_SIGNALED_BIT); // pre-signaled; first wait is a no-op + check(vkCreateFence(getDevice(), fenceInfo, null, lp)); + fence = lp.get(0); + } + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelSubmission.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelSubmission.java new file mode 100644 index 00000000..2abb14e2 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelSubmission.java @@ -0,0 +1,11 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; + +import java.util.Map; + +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.GPUBuffer; +import com.github.bsideup.jabel.Desugar; + +@Desugar +public record KernelSubmission (K key, Map inputs) { + +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelSubmissionResult.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelSubmissionResult.java new file mode 100644 index 00000000..d76f41b1 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelSubmissionResult.java @@ -0,0 +1,11 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; + +import java.util.Map; + +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.GPUBuffer; +import com.github.bsideup.jabel.Desugar; + +@Desugar +public record KernelSubmissionResult(Map outputs) { + +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelSubmissionToken.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelSubmissionToken.java new file mode 100644 index 00000000..02bb3c8a --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelSubmissionToken.java @@ -0,0 +1,8 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; + +import com.github.bsideup.jabel.Desugar; + +@Desugar +public record KernelSubmissionToken(KernelExecutor executor, Object key, int id, int level) { + +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/Noise2DKernelExecutor.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/Noise2DKernelExecutor.java new file mode 100644 index 00000000..b8f0551b --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/Noise2DKernelExecutor.java @@ -0,0 +1,63 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; + +import java.util.Map; + +import net.minecraft.world.ChunkCoordIntPair; + +import org.jetbrains.annotations.NotNull; + +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferDataType; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferLayout; +import com.cardinalstar.cubicchunks.world.worldgen.noise.NoiseSampler; +import com.google.common.collect.ImmutableMap; + +public class Noise2DKernelExecutor extends StandardKernelExecutor { + + @NotNull + private final NoiseSampler sampler; + + public Noise2DKernelExecutor(@NotNull NoiseSampler sampler) { + this.sampler = sampler; + } + + @Override + protected String generateKernel(KernelBuilder builder) { + builder.addParameter(BufferDataType.f32, "offsetX"); + builder.addParameter(BufferDataType.f32, "offsetY"); + + builder.addOutputBuffer("noise", new BufferLayout(BufferDataType.f32, 16, 16)); + + String result2d = sampler.compileKernel2D(builder, "gx", "gy"); + + return """ + #version 460 + + layout(local_size_x = 16, local_size_y = 16) in; + + layout(set = 0, binding = 0) readonly buffer Constants { uint constants[]; }; + layout(set = 1, binding = 0) buffer Arena { uint arena[]; }; + + $pc + + $preamble + + void main() { + uint x = gl_GlobalInvocationID.x; + uint y = gl_GlobalInvocationID.y; + + float gx = GET_OFFSET_X + float(x); + float gy = GET_OFFSET_Y + float(y); + + $logic + + SET_NOISE((y << 4u) | x, $result); + } + """.replace("$logic", builder.logic.toString()) + .replace("$result", result2d); + } + + @Override + protected Map getParameters(ChunkCoordIntPair key) { + return ImmutableMap.of("offsetX", (float) (key.chunkXPos << 4), "offsetY", (float) (key.chunkZPos << 4)); + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/PrimitiveBuffer.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/PrimitiveBuffer.java new file mode 100644 index 00000000..be55282b --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/PrimitiveBuffer.java @@ -0,0 +1,58 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; + +import java.nio.ByteBuffer; +import java.util.Iterator; + +import gnu.trove.procedure.TIntObjectProcedure; + +/// A wrapper around some sort of non-object buffer ([ByteBuffer] or primitive array). +/// This buffer provides a 'view' object, which contains an index into the buffer. +/// Mutations to the view are immediately reflected into the buffer. +/// Accesses on the view directly operate on the data in the buffer. +/// Views are typically pooled, but this does not have to be the case. +/// Views track whether they came from the pool. Releasing an unpooled view does nothing. +/// A primitive buffer can be fixed length or variable length - this is implementation-defined. +public interface PrimitiveBuffer { + + /// Fills this buffer with default values, but does not change its size. + void clear(); + + /// Gets the number of available entries. Entries may contain invalid data. + int size(); + + /// Gets the number of bytes used by this buffer. + int getByteLength(); + + /// Allocates a new [View], points it to the backing buffer, and returns it. + /// The [View] may come from an internal object pool and should be released by calling ([AutoCloseable#close()]). + View get(int index); + + /// Scans over this buffer and calls the function once per stored object. Each [View] is a new object. + void forEachSlow(TIntObjectProcedure fn); + + /// Scans over this buffer and calls the function once per stored object. The [View] is one object that is suitably + /// mutated. + void forEachFast(TIntObjectProcedure fn); + + /// Iterates over all contained objects. Allocates a new [View] for each object. + Iterator iteratorSlow(); + + default Iterable iterableSlow() { + return this::iteratorSlow; + } + + /// Iterates over all contained objects. The [View] is one object that is suitably mutated. + Iterator iteratorFast(); + + default Iterable iterableFast() { + return this::iteratorFast; + } + + /// Uploads the contents of this buffer into another buffer. + /// @throws IllegalArgumentException If the byte lengths don't match. + void upload(ByteBuffer dest); + + /// Downloads the contents of another buffer into this buffer. + /// @throws IllegalArgumentException If the byte lengths don't match. + void download(ByteBuffer source); +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/PrimitiveView.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/PrimitiveView.java new file mode 100644 index 00000000..e609e154 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/PrimitiveView.java @@ -0,0 +1,6 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; + +public interface PrimitiveView extends AutoCloseable { + + int getIndex(); +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/PushConstantLayout.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/PushConstantLayout.java new file mode 100644 index 00000000..28c6a8a6 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/PushConstantLayout.java @@ -0,0 +1,287 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; + +import static org.lwjgl.vulkan.VK10.VK_SHADER_STAGE_COMPUTE_BIT; +import static org.lwjgl.vulkan.VK10.vkCmdPushConstants; + +import java.nio.ByteOrder; +import java.nio.IntBuffer; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +import org.lwjgl.system.MemoryStack; +import org.lwjgl.vulkan.VkCommandBuffer; + +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferAccessor; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferDataType; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.GPUBuffer; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.OffsetBufferAccessor; +import com.github.bsideup.jabel.Desugar; + +import me.eigenraven.lwjgl3ify.api.Lwjgl3Aware; + +@Lwjgl3Aware +public class PushConstantLayout { + + public interface PushConstant { + + int getWordOffset(); + + int getWordSize(); + + void generate(StringBuilder buffer); + + void update(IntBuffer data, Map inputs, Map parameters); + } + + @Desugar + public record ArenaOffsetPushConstant(String bufferName, String pcName, int pcWordOffset) implements PushConstant { + + @Override + public int getWordOffset() { + return pcWordOffset; + } + + @Override + public int getWordSize() { + return 1; + } + + @Override + public void generate(StringBuilder buffer) { + buffer.append(" uint ") + .append(pcName) + .append(";\n"); + } + + @Override + public void update(IntBuffer data, Map inputs, Map parameters) { + GPUBuffer buffer = inputs.get(bufferName); + + data.put(pcWordOffset, buffer.getBufferOffset() / 4); + } + } + + @Desugar + public record ConstantOffsetPushConstant(int constantByteOffset, String pcName, int pcWordOffset) + implements PushConstant { + + @Override + public int getWordOffset() { + return pcWordOffset; + } + + @Override + public int getWordSize() { + return 1; + } + + @Override + public void generate(StringBuilder buffer) { + buffer.append(" uint ") + .append(pcName) + .append(";\n"); + } + + @Override + public void update(IntBuffer data, Map inputs, Map parameters) { + data.put(pcWordOffset, constantByteOffset / 4); + } + } + + @Desugar + public record ParameterPushConstant(BufferDataType dataType, String paramName, String pcName, int pcWordOffset) + implements PushConstant { + + @Override + public int getWordOffset() { + return pcWordOffset; + } + + @Override + public int getWordSize() { + return dataType.width() / 4; + } + + @Override + public void generate(StringBuilder buffer) { + String glslType = switch (dataType) { + case i32 -> "int"; + case u32 -> "uint"; + case i64 -> "long"; + case u64 -> "ulong"; + case f32 -> "float"; + case f64 -> "double"; + }; + + buffer.append(" ") + .append(glslType) + .append(" ") + .append(pcName) + .append(";\n"); + } + + @Override + public void update(IntBuffer data, Map inputs, Map parameters) { + switch (dataType) { + case i32, u32 -> { + data.put(pcWordOffset, (int) parameters.get(paramName)); + } + case i64, u64, f64 -> { + long value; + + if (dataType == BufferDataType.f64) { + value = Double.doubleToLongBits((double) parameters.get(paramName)); + } else { + value = (long) parameters.get(paramName); + } + + int lowerOffset, upperOffset; + + if (ByteOrder.nativeOrder() == ByteOrder.BIG_ENDIAN) { + upperOffset = 0; + lowerOffset = 1; + } else { + lowerOffset = 0; + upperOffset = 1; + } + + int lower = (int) value; + int upper = (int) (value >> 32); + + data.put(pcWordOffset + lowerOffset, lower); + data.put(pcWordOffset + upperOffset, upper); + } + case f32 -> { + data.put(pcWordOffset, Float.floatToIntBits((float) parameters.get(paramName))); + } + } + } + } + + private final List pushConstants = new ArrayList<>(); + + private int findSlot(int wordSize) { + BitSet taken = new BitSet(); + + for (var pc : pushConstants) { + for (int i = 0; i < pc.getWordSize(); i++) { + taken.set(pc.getWordOffset() + i); + } + } + + // Find a contiguous block of [wordSize] free words, so that the index is properly aligned + outer: for (int i = 0; i < 32; i += wordSize) { + for (int o = 0; o < wordSize; o++) { + if (taken.get(i + o)) continue outer; + } + + return i; + } + + return -1; + } + + public BufferAccessor addArenaOffset(BufferDataType dataType, String bufferName) { + int offset = findSlot(1); + + if (offset == -1) { + throw new IllegalStateException( + "All push constant slots are full: cannot allocate another one: " + bufferName); + } + + String pcName = "arenaOffset" + pushConstants.size(); + + pushConstants.add(new ArenaOffsetPushConstant(bufferName, pcName, offset)); + + return new OffsetBufferAccessor("arena", pcName, dataType); + } + + public BufferAccessor addConstantOffset(BufferDataType dataType, int constantByteOffset) { + int offset = findSlot(1); + + if (offset == -1) { + throw new IllegalStateException( + "All push constant slots are full: cannot allocate another one: " + constantByteOffset); + } + + String pcName = "constantOffset" + pushConstants.size(); + + pushConstants.add(new ConstantOffsetPushConstant(constantByteOffset, pcName, offset)); + + return new OffsetBufferAccessor("constants", pcName, dataType); + } + + public String addParameter(BufferDataType dataType, String paramName) { + int offset = findSlot(dataType.width() / 4); + + if (offset == -1) { + throw new IllegalStateException( + "All push constant slots are full: cannot allocate another one: " + paramName); + } + + String pcName = paramName + pushConstants.size(); + + pushConstants.add(new ParameterPushConstant(dataType, paramName, pcName, offset)); + + return "pc." + pcName; + } + + public String getPushConstantDefinition() { + StringBuilder text = new StringBuilder(); + + text.append("layout(push_constant) uniform PC {\n"); + + // Find the highest word index used so we don't iterate over empty tail. + int highWater = 0; + for (PushConstant pc : pushConstants) { + highWater = Math.max(highWater, pc.getWordOffset() + pc.getWordSize()); + } + + PushConstant[] slots = new PushConstant[highWater]; + for (PushConstant pc : pushConstants) { + for (int word = 0; word < pc.getWordSize(); word++) { + slots[pc.getWordOffset() + word] = pc; + } + } + + HashSet emitted = new HashSet<>(); + int padCounter = 0; + + for (int i = 0; i < slots.length; i++) { + PushConstant pc = slots[i]; + + if (pc == null) { + // Gap between entries — pad to maintain correct offsets. + text.append(" uint __padding") + .append(padCounter++) + .append(";\n"); + continue; + } + + // Already emitted the declaration for this PC (multi-word entry). + if (!emitted.add(pc)) continue; + + pc.generate(text); + } + + text.append("} pc;\n"); + + return text.toString(); + } + + public void upload(VkCommandBuffer commands, long pipelineLayout, Map inputs, + Map parameters) { + try (MemoryStack stack = MemoryStack.stackPush()) { + IntBuffer data = stack.callocInt(32); + + for (var pc : pushConstants) { + pc.update(data, inputs, parameters); + } + + vkCmdPushConstants(commands, pipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, data); + } + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/RenderdocAPI.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/RenderdocAPI.java new file mode 100644 index 00000000..1741b607 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/RenderdocAPI.java @@ -0,0 +1,18 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; + +import com.cardinalstar.cubicchunks.CubicChunks; + +public class RenderdocAPI { + + private static boolean available = false; + + static { + try { + System.loadLibrary("renderdoc"); + available = true; + } catch (UnsatisfiedLinkError e) { + CubicChunks.LOGGER.warn("Failed to load renderdoc native library", e); + } + } + +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/ShaderCache.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/ShaderCache.java new file mode 100644 index 00000000..e8a907b5 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/ShaderCache.java @@ -0,0 +1,157 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.LocalDate; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import com.cardinalstar.cubicchunks.CubicChunks; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +import me.eigenraven.lwjgl3ify.api.Lwjgl3Aware; + +@Lwjgl3Aware +public class ShaderCache { + + private static File shadersDir; + private static Map index; + private static final Gson GSON = new Gson(); + private static final long EXPIRY_DAYS = 30; + + private ShaderCache() {} + + public static void init(File cacheDir) { + shadersDir = new File(cacheDir, "shaders"); + if (!shadersDir.exists()) { + shadersDir.mkdirs(); + } + loadIndex(); + cleanup(); + } + + public static byte[] getOrCompile(String glslSource) { + String hash = sha256Hex(glslSource); + File spvFile = new File(shadersDir, hash + ".spv"); + + if (spvFile.exists()) { + try { + byte[] bytes = Files.readAllBytes(spvFile.toPath()); + long today = LocalDate.now() + .toEpochDay(); + index.put(hash, today); + saveIndex(); + return bytes; + } catch (IOException e) { + CubicChunks.LOGGER.warn("Failed to read cached SPIR-V {}, recompiling", spvFile, e); + } + } + + // Cache miss — compile + byte[] bytes = SpirVCompiler.compile(glslSource); + + try { + Files.write(spvFile.toPath(), bytes); + } catch (IOException e) { + CubicChunks.LOGGER.warn("Failed to write SPIR-V cache file {}", spvFile, e); + } + + long today = LocalDate.now() + .toEpochDay(); + index.put(hash, today); + saveIndex(); + + return bytes; + } + + private static void loadIndex() { + File indexFile = new File(shadersDir, "index.json"); + if (indexFile.exists()) { + try { + String json = new String(Files.readAllBytes(indexFile.toPath()), StandardCharsets.UTF_8); + Map loaded = GSON.fromJson(json, new TypeToken>() {}.getType()); + index = (loaded != null) ? loaded : new HashMap<>(); + return; + } catch (Exception e) { + CubicChunks.LOGGER.warn("Failed to read shader cache index, clearing cache", e); + } + } else { + CubicChunks.LOGGER.warn("Shader cache index not found, starting fresh"); + } + + // Delete all existing .spv files since we can't trust them without the index + File[] spvFiles = shadersDir.listFiles((dir, name) -> name.endsWith(".spv")); + if (spvFiles != null) { + for (File f : spvFiles) { + f.delete(); + } + } + index = new HashMap<>(); + } + + private static void saveIndex() { + File indexFile = new File(shadersDir, "index.json"); + try { + Files.write( + indexFile.toPath(), + GSON.toJson(index) + .getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + CubicChunks.LOGGER.warn("Failed to save shader cache index", e); + } + } + + private static void cleanup() { + long today = LocalDate.now() + .toEpochDay(); + long cutoff = today - EXPIRY_DAYS; + + boolean modified = false; + Iterator> it = index.entrySet() + .iterator(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + if (entry.getValue() < cutoff) { + it.remove(); + new File(shadersDir, entry.getKey() + ".spv").delete(); + modified = true; + } + } + + // Orphan cleanup: remove .spv files whose hash isn't in the index + File[] spvFiles = shadersDir.listFiles((dir, name) -> name.endsWith(".spv")); + if (spvFiles != null) { + for (File f : spvFiles) { + String name = f.getName(); + String hashPart = name.substring(0, name.length() - 4); // strip ".spv" + if (!index.containsKey(hashPart)) { + f.delete(); + } + } + } + + if (modified) { + saveIndex(); + } + } + + private static String sha256Hex(String input) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(hash.length * 2); + for (byte b : hash) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("SHA-256 not available", e); + } + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/SpirVCompiler.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/SpirVCompiler.java new file mode 100644 index 00000000..1ce11c54 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/SpirVCompiler.java @@ -0,0 +1,75 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; + +import static org.lwjgl.util.shaderc.Shaderc.shaderc_compilation_status_success; +import static org.lwjgl.util.shaderc.Shaderc.shaderc_compile_into_spv; +import static org.lwjgl.util.shaderc.Shaderc.shaderc_compiler_initialize; +import static org.lwjgl.util.shaderc.Shaderc.shaderc_compiler_release; +import static org.lwjgl.util.shaderc.Shaderc.shaderc_glsl_compute_shader; +import static org.lwjgl.util.shaderc.Shaderc.shaderc_result_get_bytes; +import static org.lwjgl.util.shaderc.Shaderc.shaderc_result_get_compilation_status; +import static org.lwjgl.util.shaderc.Shaderc.shaderc_result_get_error_message; +import static org.lwjgl.util.shaderc.Shaderc.shaderc_result_release; + +import java.nio.ByteBuffer; + +import net.minecraft.util.MathHelper; + +import com.cardinalstar.cubicchunks.CubicChunks; + +import me.eigenraven.lwjgl3ify.api.Lwjgl3Aware; + +@Lwjgl3Aware +public class SpirVCompiler { + + private static long compiler; + + private SpirVCompiler() {} + + public static void init() { + compiler = shaderc_compiler_initialize(); + if (compiler == 0) { + throw new IllegalStateException("Failed to initialize shaderc compiler"); + } + } + + public static void destroy() { + if (compiler != 0) { + shaderc_compiler_release(compiler); + compiler = 0; + } + } + + public static byte[] compile(String glsl) { + long result = 0; + try { + result = shaderc_compile_into_spv(compiler, glsl, shaderc_glsl_compute_shader, "shader.comp", "main", 0); + + if (shaderc_result_get_compilation_status(result) != shaderc_compilation_status_success) { + String errorMessage = shaderc_result_get_error_message(result); + + String[] lines = glsl.split("\n\r?"); + int zeroes = MathHelper.ceiling_double_int(Math.log10(lines.length)); + for (int i = 0; i < lines.length; i++) { + lines[i] = String.format("%0" + zeroes + "d:", i + 1) + lines[i]; + } + + CubicChunks.LOGGER.error("Shader code:\n{}", String.join("\n", lines)); + CubicChunks.LOGGER.error("Could not compile shader to SPIR-V: {}", errorMessage, new Exception()); + throw new RuntimeException("Could not compile shader to SPIR-V"); + } + + ByteBuffer spirvBytes = shaderc_result_get_bytes(result); + if (spirvBytes == null) { + throw new RuntimeException("shaderc returned null SPIR-V bytes despite success status"); + } + + byte[] bytes = new byte[spirvBytes.remaining()]; + spirvBytes.get(bytes); + return bytes; + } finally { + if (result != 0) { + shaderc_result_release(result); + } + } + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/StandardKernelExecutor.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/StandardKernelExecutor.java new file mode 100644 index 00000000..49d32808 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/StandardKernelExecutor.java @@ -0,0 +1,115 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; + +import static org.lwjgl.vulkan.VK10.vkCmdDispatch; + +import java.util.HashMap; +import java.util.Map; + +import org.lwjgl.vulkan.VkCommandBuffer; + +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferAllocator; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferDescriptor; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferLayout; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.ConstantBuffer; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.GPUBuffer; +import com.google.common.collect.ImmutableMap; + +import me.eigenraven.lwjgl3ify.api.Lwjgl3Aware; + +@Lwjgl3Aware +public abstract class StandardKernelExecutor implements KernelExecutor { + + private final Map inputs = new HashMap<>(), outputs = new HashMap<>(); + + private ComputePipeline pipeline; + private PushConstantLayout pushConstants; + + @Override + public boolean isCompiled() { + return pipeline != null; + } + + @Override + public void compile(ConstantBuffer constants) { + KernelBuilder builder = new KernelBuilder(constants); + + String code = generateKernel(builder); + + code = code.replace("$preamble", builder.preamble) + .replace("$pc", builder.pushConstants.getPushConstantDefinition()); + + this.pipeline = new ComputePipeline(this.toString(), code); + this.pushConstants = builder.pushConstants; + this.inputs.putAll(builder.inputs); + this.outputs.putAll(builder.outputs); + } + + protected abstract String generateKernel(KernelBuilder builder); + + @Override + public void close() { + pipeline.destroy(); + } + + @Override + public Map getOutputs(ComputePlan plan, KernelSubmissionToken submission, Key key, + Map inputs) { + + this.inputs.forEach( + (name, layout) -> { + inputs.get(name) + .assertLayout(layout); + }); + + var outputs = ImmutableMap.builder(); + + this.outputs.forEach((name, layout) -> { outputs.put(name, plan.describeBuffer(submission, layout)); }); + + return outputs.build(); + } + + @Override + public KernelSubmissionResult[] submit(VkCommandBuffer commands, BufferAllocator alloc, + KernelSubmission[] submissions) { + pipeline.bind(commands); + + KernelSubmissionResult[] results = new KernelSubmissionResult[submissions.length]; + + for (int i = 0; i < submissions.length; i++) { + var outputs = ImmutableMap.builder(); + + this.outputs.forEach((name, layout) -> { + GPUBuffer buffer = alloc.alloc(layout); + + outputs.put(name, buffer); + }); + + var outputMap = outputs.build(); + + results[i] = new KernelSubmissionResult(outputMap); + + Map buffers = new HashMap<>(); + buffers.putAll(submissions[i].inputs()); + buffers.putAll(outputMap); + + pushConstants.upload( + commands, + KernelContext.getScheduler() + .getPipelineLayout(), + buffers, + getParameters(submissions[i].key())); + + dispatch(commands); + } + + return results; + } + + protected Map getParameters(Key key) { + return ImmutableMap.of(); + } + + protected void dispatch(VkCommandBuffer commands) { + vkCmdDispatch(commands, 1, 1, 1); + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/Terminal.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/Terminal.java new file mode 100644 index 00000000..b3f4992f --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/Terminal.java @@ -0,0 +1,11 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; + +import java.util.Map; + +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferDescriptor; +import com.github.bsideup.jabel.Desugar; + +@Desugar +record Terminal(Map inputs, TerminalTask task) { + +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/TerminalTask.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/TerminalTask.java new file mode 100644 index 00000000..0aa2fdf8 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/TerminalTask.java @@ -0,0 +1,9 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; + +import java.nio.ByteBuffer; +import java.util.Map; + +public interface TerminalTask { + + void execute(Map inputs); +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/VulkanBuffer.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/VulkanBuffer.java new file mode 100644 index 00000000..2017e20b --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/VulkanBuffer.java @@ -0,0 +1,229 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; + +import static org.lwjgl.system.MemoryStack.stackPush; +import static org.lwjgl.system.MemoryUtil.memByteBuffer; +import static org.lwjgl.util.vma.Vma.VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT; +import static org.lwjgl.util.vma.Vma.VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT; +import static org.lwjgl.util.vma.Vma.VMA_ALLOCATION_CREATE_MAPPED_BIT; +import static org.lwjgl.util.vma.Vma.VMA_MEMORY_USAGE_AUTO; +import static org.lwjgl.util.vma.Vma.vmaCreateBuffer; +import static org.lwjgl.util.vma.Vma.vmaDestroyBuffer; +import static org.lwjgl.vulkan.VK10.VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; +import static org.lwjgl.vulkan.VK10.VK_BUFFER_USAGE_TRANSFER_DST_BIT; +import static org.lwjgl.vulkan.VK10.VK_BUFFER_USAGE_TRANSFER_SRC_BIT; + +import java.io.Closeable; +import java.nio.ByteBuffer; +import java.nio.LongBuffer; + +import org.lwjgl.PointerBuffer; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.util.vma.VmaAllocationCreateInfo; +import org.lwjgl.util.vma.VmaAllocationInfo; +import org.lwjgl.vulkan.VkBufferCreateInfo; + +import me.eigenraven.lwjgl3ify.api.Lwjgl3Aware; + +/** + * A host-visible, persistently mapped Vulkan buffer backed by VMA. + * + *

+ * Create instances via the static factory methods: + *

    + *
  • {@link #allocDeviceLocal(long, int)} — device-local SSBO (GPU only, fast for compute)
  • + *
  • {@link #allocHostVisible(long, int)} — host-visible, coherent buffer for CPU↔GPU transfers
  • + *
+ * + *

+ * The buffer is persistently mapped: {@link #mapped()} always returns the same + * {@link ByteBuffer} view. Callers must not retain the returned buffer past {@link #close()}. + */ +@Lwjgl3Aware +public final class VulkanBuffer implements Closeable { + + /** Raw Vulkan buffer handle. */ + private final long buffer; + /** VMA allocation handle. */ + private final long allocation; + /** Size in bytes of this buffer. */ + private final int byteLen; + /** Persistently mapped view, or {@code null} for device-local buffers. */ + private final ByteBuffer mappedView; + + // Stored so resize() can recreate an equivalent buffer. + private final int usageFlags; + private final int vmaUsage; + private final int vmaCreateFlags; + + private boolean closed = false; + + private VulkanBuffer(long buffer, long allocation, int byteLen, ByteBuffer mappedView, int usageFlags, int vmaUsage, + int vmaCreateFlags) { + this.buffer = buffer; + this.allocation = allocation; + this.byteLen = byteLen; + this.mappedView = mappedView; + this.usageFlags = usageFlags; + this.vmaUsage = vmaUsage; + this.vmaCreateFlags = vmaCreateFlags; + } + + // ------------------------------------------------------------------------- + // Factory methods + // ------------------------------------------------------------------------- + + /** + * Allocates a device-local storage buffer. + * + *

+ * The buffer is not host-visible; use a staging buffer to upload data. + * Usage flags: {@code STORAGE_BUFFER | TRANSFER_SRC | TRANSFER_DST}. + * + * @param allocator VMA allocator handle (from {@link KernelContext#getVmaAllocator()}) + * @param byteLen byte capacity of the buffer + * @return a newly allocated {@link VulkanBuffer} + */ + public static VulkanBuffer allocDeviceLocal(long allocator, int byteLen) { + return alloc( + allocator, + byteLen, + VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, + VMA_MEMORY_USAGE_AUTO, + 0 /* no host-access flags — device-local */); + } + + /** + * Allocates a host-visible, persistently mapped buffer suitable for staging. + * + *

+ * The returned buffer's {@link #mapped()} view is always valid for CPU read/write. + * Usage flags: {@code STORAGE_BUFFER | TRANSFER_SRC | TRANSFER_DST}. + * + * @param allocator VMA allocator handle (from {@link KernelContext#getVmaAllocator()}) + * @param byteLen byte capacity of the buffer + * @return a newly allocated, persistently mapped {@link VulkanBuffer} + */ + public static VulkanBuffer allocHostVisible(long allocator, int byteLen) { + return alloc( + allocator, + byteLen, + VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, + VMA_MEMORY_USAGE_AUTO, + VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT + | VMA_ALLOCATION_CREATE_MAPPED_BIT); + } + + private static VulkanBuffer alloc(long allocator, int byteLen, int usageFlags, int vmaUsage, int vmaFlags) { + try (MemoryStack stack = stackPush()) { + VkBufferCreateInfo bufferInfo = VkBufferCreateInfo.calloc(stack) + .sType$Default() + .size(byteLen) + .usage(usageFlags); + // sharingMode defaults to VK_SHARING_MODE_EXCLUSIVE (0) via calloc + + VmaAllocationCreateInfo allocInfo = VmaAllocationCreateInfo.calloc(stack) + .usage(vmaUsage) + .flags(vmaFlags); + + LongBuffer pBuffer = stack.mallocLong(1); + PointerBuffer pAlloc = stack.mallocPointer(1); + VmaAllocationInfo info = VmaAllocationInfo.calloc(stack); + + KernelContext.check(vmaCreateBuffer(allocator, bufferInfo, allocInfo, pBuffer, pAlloc, info)); + + long bufHandle = pBuffer.get(0); + long allocHandle = pAlloc.get(0); + + ByteBuffer mapped = null; + long pMapped = info.pMappedData(); + if (pMapped != 0L) { + mapped = memByteBuffer(pMapped, byteLen); + } + + return new VulkanBuffer(bufHandle, allocHandle, byteLen, mapped, usageFlags, vmaUsage, vmaFlags); + } + } + + // ------------------------------------------------------------------------- + // Accessors + // ------------------------------------------------------------------------- + + /** Returns the raw Vulkan buffer handle. */ + public long buffer() { + return buffer; + } + + /** Returns the VMA allocation handle. */ + public long allocation() { + return allocation; + } + + /** Returns the byte capacity of this buffer. */ + public int byteLen() { + return byteLen; + } + + /** + * Returns a persistently mapped view of this buffer's memory. + * + * @return the mapped {@link ByteBuffer}, or {@code null} if this is a device-local buffer + * @throws IllegalStateException if this buffer has been closed + */ + public ByteBuffer mapped() { + if (closed) throw new IllegalStateException("VulkanBuffer has been closed"); + return mappedView; + } + + // ------------------------------------------------------------------------- + // Lifecycle + // ------------------------------------------------------------------------- + + /** + * Returns a buffer with at least {@code newByteLen} bytes capacity. + * + *

+ * If this buffer is already large enough, returns {@code this} unchanged. + * Otherwise destroys this buffer and allocates a new one with the same type and flags. + * The caller must update any descriptor sets that reference this buffer when the + * return value is not {@code this}. + * + * @param allocator VMA allocator handle + * @param newByteLen required minimum byte capacity + * @return {@code this} if already sufficient, otherwise a new larger buffer + */ + public VulkanBuffer resize(long allocator, int newByteLen) { + if (newByteLen <= byteLen) return this; + destroy(allocator); + return alloc(allocator, newByteLen, usageFlags, vmaUsage, vmaCreateFlags); + } + + /** + * Destroys the Vulkan buffer and frees the VMA allocation. + * + *

+ * After this call, {@link #mapped()} will throw. The {@code allocator} handle + * must be the same one used to create this buffer. + * + * @param allocator VMA allocator handle + */ + public void destroy(long allocator) { + if (!closed) { + closed = true; + vmaDestroyBuffer(allocator, buffer, allocation); + } + } + + /** Alias for {@link #destroy(long)}. */ + public void close(long allocator) { + destroy(allocator); + } + + /** + * Convenience override that retrieves the allocator from {@link KernelContext}. + * Only valid while the Vulkan context is active. + */ + @Override + public void close() { + destroy(KernelContext.getVmaAllocator()); + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/VulkanPipelineCache.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/VulkanPipelineCache.java new file mode 100644 index 00000000..4147ac78 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/VulkanPipelineCache.java @@ -0,0 +1,94 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; + +import static org.lwjgl.system.MemoryUtil.memAlloc; +import static org.lwjgl.system.MemoryUtil.memFree; +import static org.lwjgl.vulkan.VK10.vkCreatePipelineCache; +import static org.lwjgl.vulkan.VK10.vkDestroyPipelineCache; +import static org.lwjgl.vulkan.VK10.vkGetPipelineCacheData; + +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.LongBuffer; +import java.nio.file.Files; + +import org.lwjgl.PointerBuffer; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.vulkan.VkPipelineCacheCreateInfo; + +import com.cardinalstar.cubicchunks.CubicChunks; + +import me.eigenraven.lwjgl3ify.api.Lwjgl3Aware; + +@Lwjgl3Aware +public class VulkanPipelineCache { + + private static long pipelineCache; + private static File savedCacheFile; + + private VulkanPipelineCache() {} + + public static void init(File cacheDir) { + if (!cacheDir.exists()) { + cacheDir.mkdirs(); + } + savedCacheFile = new File(cacheDir, "pipeline_cache.bin"); + + ByteBuffer initialData = null; + try (MemoryStack stack = MemoryStack.stackPush()) { + VkPipelineCacheCreateInfo createInfo = VkPipelineCacheCreateInfo.calloc(stack) + .sType$Default(); + + if (savedCacheFile.exists()) { + try { + byte[] bytes = Files.readAllBytes(savedCacheFile.toPath()); + initialData = memAlloc(bytes.length); + initialData.put(bytes) + .flip(); + createInfo.pInitialData(initialData); + } catch (IOException e) { + CubicChunks.LOGGER.warn("Failed to read pipeline cache, starting fresh", e); + } + } + + LongBuffer lb = stack.mallocLong(1); + KernelContext.check(vkCreatePipelineCache(KernelContext.getDevice(), createInfo, null, lb)); + pipelineCache = lb.get(0); + } finally { + if (initialData != null) memFree(initialData); + } + } + + public static void save() { + if (pipelineCache == 0 || savedCacheFile == null) return; + try (MemoryStack stack = MemoryStack.stackPush()) { + PointerBuffer pSize = stack.mallocPointer(1); + KernelContext.check(vkGetPipelineCacheData(KernelContext.getDevice(), pipelineCache, pSize, null)); + long size = pSize.get(0); + if (size == 0) return; + ByteBuffer data = memAlloc((int) size); + try { + KernelContext.check(vkGetPipelineCacheData(KernelContext.getDevice(), pipelineCache, pSize, data)); + byte[] bytes = new byte[(int) size]; + data.get(bytes); + Files.write(savedCacheFile.toPath(), bytes); + } catch (IOException e) { + CubicChunks.LOGGER.warn("Failed to save pipeline cache", e); + } finally { + memFree(data); + } + } + } + + public static void destroy() { + save(); + if (pipelineCache != 0) { + vkDestroyPipelineCache(KernelContext.getDevice(), pipelineCache, null); + pipelineCache = 0; + } + } + + public static long getCache() { + return pipelineCache; + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/BufferAccessor.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/BufferAccessor.java new file mode 100644 index 00000000..cbd438c4 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/BufferAccessor.java @@ -0,0 +1,11 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer; + +/// Something that can retrieve a value from a flat buffer/equation. +/// Accepts an int and returns whatever type this accessor returns. +public interface BufferAccessor { + + BufferDataType getDataType(); + + String access(String index); + +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/BufferAllocator.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/BufferAllocator.java new file mode 100644 index 00000000..9c7a0749 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/BufferAllocator.java @@ -0,0 +1,18 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer; + +public interface BufferAllocator { + + default GPUBuffer alloc(BufferDataType dataType, int lenX) { + return alloc(dataType, lenX, 1, 1); + } + + default GPUBuffer alloc(BufferDataType dataType, int lenX, int lenY) { + return alloc(dataType, lenX, lenY, 1); + } + + default GPUBuffer alloc(BufferLayout layout) { + return alloc(layout.dataType(), layout.lenX(), layout.lenY(), layout.lenZ()); + } + + GPUBuffer alloc(BufferDataType dataType, int lenX, int lenY, int lenZ); +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/BufferDataType.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/BufferDataType.java new file mode 100644 index 00000000..e3474471 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/BufferDataType.java @@ -0,0 +1,38 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer; + +public enum BufferDataType { + + i32, + u32, + i64, + u64, + f32, + f64; + + public int width() { + return switch (this) { + case i32, u32, f32 -> 4; + case i64, u64, f64 -> 8; + }; + } + + public String fromUint(String expr) { + return switch (this) { + case i32 -> "int(" + expr + ")"; + case u32 -> expr; + case f32 -> "uintBitsToFloat(" + expr + ")"; + case i64, u64, f64 -> throw new UnsupportedOperationException( + "64-bit GLSL buffer accessors require two uint32 slots and are not yet implemented: " + this); + }; + } + + public String toUint(String expr) { + return switch (this) { + case i32 -> "uint(" + expr + ")"; + case u32 -> expr; + case f32 -> "floatBitsToUint(" + expr + ")"; + case i64, u64, f64 -> throw new UnsupportedOperationException( + "64-bit GLSL buffer accessors require two uint32 slots and are not yet implemented: " + this); + }; + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/BufferDescriptor.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/BufferDescriptor.java new file mode 100644 index 00000000..adba3dcc --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/BufferDescriptor.java @@ -0,0 +1,37 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer; + +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelSubmissionToken; +import com.github.bsideup.jabel.Desugar; + +@Desugar +public record BufferDescriptor(KernelSubmissionToken submission, int bufferId, BufferDataType dataType, int lenX, + int lenY, int lenZ) { + + public void assertLayout(BufferDataType dataType, int lenX) { + assertLayout(dataType, lenX, 1, 1); + } + + public void assertLayout(BufferDataType dataType, int lenX, int lenY) { + assertLayout(dataType, lenX, lenY, 1); + } + + public void assertLayout(BufferDataType dataType, int lenX, int lenY, int lenZ) { + if (this.dataType != dataType) throw new IllegalStateException( + "Expected buffer to contain " + dataType + " but it contains " + this.dataType); + + if (this.lenX != lenX) + throw new IllegalStateException("Expected buffer X length to be " + lenX + " but it was " + this.lenX); + if (this.lenY != lenY) + throw new IllegalStateException("Expected buffer Y length to be " + lenY + " but it was " + this.lenY); + if (this.lenZ != lenZ) + throw new IllegalStateException("Expected buffer Z length to be " + lenZ + " but it was " + this.lenZ); + } + + public void assertLayout(BufferLayout layout) { + assertLayout(layout.dataType(), layout.lenX(), layout.lenY(), layout.lenZ()); + } + + public int getBufferLength() { + return dataType.width() * lenX * lenY * lenZ; + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/BufferLayout.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/BufferLayout.java new file mode 100644 index 00000000..cb5f2e56 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/BufferLayout.java @@ -0,0 +1,15 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer; + +import com.github.bsideup.jabel.Desugar; + +@Desugar +public record BufferLayout(BufferDataType dataType, int lenX, int lenY, int lenZ) { + + public BufferLayout(BufferDataType dataType, int lenX, int lenY) { + this(dataType, lenX, lenY, 1); + } + + public BufferLayout(BufferDataType dataType, int lenX) { + this(dataType, lenX, 1, 1); + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/ConstantBuffer.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/ConstantBuffer.java new file mode 100644 index 00000000..c99f38ad --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/ConstantBuffer.java @@ -0,0 +1,67 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; + +public interface ConstantBuffer { + + GPUBuffer addConstant(BufferDataType dataType, ByteBuffer data); + + default GPUBuffer addConstant(IntBuffer data) { + ByteBuffer bytes = ByteBuffer.allocateDirect(data.remaining() * 4) + .order(ByteOrder.nativeOrder()); + + int rem = data.remaining(); + + for (int i = 0; i < rem; i++) { + int value = data.get(data.position() + i); + + bytes.putInt(i * 4, value); + } + + return addConstant(BufferDataType.i32, bytes); + } + + default GPUBuffer addConstant(int[] data) { + ByteBuffer bytes = ByteBuffer.allocateDirect(data.length * 4) + .order(ByteOrder.nativeOrder()); + + int rem = data.length; + + for (int i = 0; i < rem; i++) { + bytes.putInt(i * 4, data[i]); + } + + return addConstant(BufferDataType.i32, bytes); + } + + default GPUBuffer addConstant(FloatBuffer data) { + ByteBuffer bytes = ByteBuffer.allocateDirect(data.remaining() * 4) + .order(ByteOrder.nativeOrder()); + + int rem = data.remaining(); + + for (int i = 0; i < rem; i++) { + float value = data.get(data.position() + i); + + bytes.putFloat(i * 4, value); + } + + return addConstant(BufferDataType.f32, bytes); + } + + default GPUBuffer addConstant(float[] data) { + ByteBuffer bytes = ByteBuffer.allocateDirect(data.length * 4) + .order(ByteOrder.nativeOrder()); + + int rem = data.length; + + for (int i = 0; i < rem; i++) { + bytes.putFloat(i * 4, data[i]); + } + + return addConstant(BufferDataType.f32, bytes); + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/GPUBuffer.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/GPUBuffer.java new file mode 100644 index 00000000..1a04132b --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/GPUBuffer.java @@ -0,0 +1,18 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer; + +public interface GPUBuffer { + + BufferDataType getDataType(); + + int getBufferOffset(); + + default int getBufferLength() { + return getDataType().width() * getLenX() * getLenY() * getLenZ(); + } + + int getLenX(); + + int getLenY(); + + int getLenZ(); +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/OffsetBufferAccessor.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/OffsetBufferAccessor.java new file mode 100644 index 00000000..8af5fb62 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/OffsetBufferAccessor.java @@ -0,0 +1,23 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer; + +public class OffsetBufferAccessor implements BufferAccessor { + + public final String buffer, pcName; + public final BufferDataType dataType; + + public OffsetBufferAccessor(String buffer, String pcName, BufferDataType dataType) { + this.buffer = buffer; + this.pcName = pcName; + this.dataType = dataType; + } + + @Override + public BufferDataType getDataType() { + return dataType; + } + + @Override + public String access(String index) { + return buffer + "[pc." + pcName + " + (" + index + ")]"; + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/TransformingBufferAccessor.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/TransformingBufferAccessor.java new file mode 100644 index 00000000..e7da1d10 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/TransformingBufferAccessor.java @@ -0,0 +1,24 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer; + +import java.util.function.Function; + +public class TransformingBufferAccessor implements BufferAccessor { + + private final BufferAccessor next; + private final Function modifyIndex; + + public TransformingBufferAccessor(BufferAccessor next, Function modifyIndex) { + this.next = next; + this.modifyIndex = modifyIndex; + } + + @Override + public BufferDataType getDataType() { + return next.getDataType(); + } + + @Override + public String access(String index) { + return next.access(modifyIndex.apply(index)); + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/VulkanArenaAllocator.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/VulkanArenaAllocator.java new file mode 100644 index 00000000..181996ed --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/VulkanArenaAllocator.java @@ -0,0 +1,36 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer; + +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelContext; +import com.cardinalstar.cubicchunks.util.MathUtil; + +/** + * Offset-tracking allocator over the Vulkan device-local arena buffer. + * + *

+ * Allocations are sub-regions of {@code KernelContext.arenaBuffer}. Each call to + * {@link #alloc} advances an internal byte cursor; {@link #reset} sets it back to zero + * for the next batch. No memory is actually allocated or freed here — the backing buffer + * is managed by {@link KernelContext#ensureArenaCapacity}. + */ +public class VulkanArenaAllocator implements BufferAllocator { + + private int nextByteOffset = 0; + + @Override + public GPUBuffer alloc(BufferDataType dataType, int lenX, int lenY, int lenZ) { + int byteLen = dataType.width() * lenX * lenY * lenZ; + int offset = nextByteOffset; + nextByteOffset = MathUtil.alignTo(nextByteOffset + byteLen, 16); + return new VulkanArenaSlot(dataType, offset, lenX, lenY, lenZ); + } + + /** Resets the allocation cursor to zero. Call once at the start of each batch. */ + public void reset() { + nextByteOffset = 0; + } + + /** Returns the current high-water mark in bytes (useful for {@code ensureArenaCapacity}). */ + public int currentByteLen() { + return nextByteOffset; + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/VulkanArenaSlot.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/VulkanArenaSlot.java new file mode 100644 index 00000000..2e1523eb --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/VulkanArenaSlot.java @@ -0,0 +1,52 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer; + +/** + * An arena slot in the Vulkan device-local arena buffer. + * + *

+ * Carries the byte offset into the arena buffer and the logical dimensions of the + * allocation. The actual memory is owned by {@code KernelContext.arenaBuffer} — closing + * this slot is a no-op. + */ +public final class VulkanArenaSlot implements GPUBuffer { + + private final BufferDataType dataType; + private final int byteOffset; + private final int lenX; + private final int lenY; + private final int lenZ; + + VulkanArenaSlot(BufferDataType dataType, int byteOffset, int lenX, int lenY, int lenZ) { + this.dataType = dataType; + this.byteOffset = byteOffset; + this.lenX = lenX; + this.lenY = lenY; + this.lenZ = lenZ; + } + + @Override + public BufferDataType getDataType() { + return dataType; + } + + /** Returns the byte offset of this slot within the arena VkBuffer. */ + @Override + public int getBufferOffset() { + return byteOffset; + } + + @Override + public int getLenX() { + return lenX; + } + + @Override + public int getLenY() { + return lenY; + } + + @Override + public int getLenZ() { + return lenZ; + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/VulkanConstantPool.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/VulkanConstantPool.java new file mode 100644 index 00000000..45dd098e --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/VulkanConstantPool.java @@ -0,0 +1,122 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer; + +import static org.lwjgl.util.vma.Vma.vmaFlushAllocation; +import static org.lwjgl.vulkan.VK10.VK_ACCESS_SHADER_READ_BIT; +import static org.lwjgl.vulkan.VK10.VK_ACCESS_TRANSFER_WRITE_BIT; +import static org.lwjgl.vulkan.VK10.VK_QUEUE_FAMILY_IGNORED; + +import java.nio.ByteBuffer; + +import org.lwjgl.system.MemoryStack; +import org.lwjgl.system.MemoryUtil; +import org.lwjgl.vulkan.VK10; +import org.lwjgl.vulkan.VkBufferCopy; +import org.lwjgl.vulkan.VkBufferMemoryBarrier; +import org.lwjgl.vulkan.VkCommandBuffer; + +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelContext; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.VulkanBuffer; +import com.cardinalstar.cubicchunks.util.MathUtil; + +import lombok.Getter; +import me.eigenraven.lwjgl3ify.api.Lwjgl3Aware; + +@Lwjgl3Aware +public class VulkanConstantPool implements ConstantBuffer { + + private final long allocator; + + private long dataPointer; + private int dataCapacity, dataLength; + private boolean dirty; + + private VulkanBuffer hostBuffer; + @Getter + private VulkanBuffer deviceBuffer; + + public VulkanConstantPool(long allocator) { + this.allocator = allocator; + // Placeholder so bindDescriptorBuffers() can always bind a valid buffer, + // even before any executor has added constants. + deviceBuffer = VulkanBuffer.allocDeviceLocal(allocator, 4); + } + + @Override + public GPUBuffer addConstant(BufferDataType dataType, ByteBuffer data) { + int offset = dataLength; + int newEnd = MathUtil.alignTo(dataLength + data.remaining(), 16); + + if (newEnd > dataCapacity) { + dataCapacity += KernelContext.CHUNK_SIZE; + + if (dataPointer != 0) { + dataPointer = MemoryUtil.nmemRealloc(dataPointer, dataCapacity); + } else { + dataPointer = MemoryUtil.nmemAlloc(dataCapacity); + } + } + + ByteBuffer dst = MemoryUtil.memByteBuffer(dataPointer + offset, dataCapacity - offset); + + if (data.isDirect()) { + MemoryUtil.memCopy(data, dst); + } else { + dst.put(data); + } + + dataLength = newEnd; + dirty = true; + + return new VulkanArenaSlot(dataType, offset, data.remaining() / dataType.width(), 1, 1); + } + + public void update(VkCommandBuffer commands) { + try (MemoryStack stack = MemoryStack.stackPush()) { + if (!dirty) return; + dirty = false; + + if (hostBuffer == null) { + hostBuffer = VulkanBuffer.allocHostVisible(allocator, dataCapacity); + } else if (hostBuffer.byteLen() < dataCapacity) { + hostBuffer.destroy(allocator); + hostBuffer = VulkanBuffer.allocHostVisible(allocator, dataCapacity); + } + + if (deviceBuffer == null) { + deviceBuffer = VulkanBuffer.allocDeviceLocal(allocator, dataCapacity); + } else if (deviceBuffer.byteLen() < dataCapacity) { + deviceBuffer.destroy(allocator); + deviceBuffer = VulkanBuffer.allocDeviceLocal(allocator, dataCapacity); + } + + MemoryUtil.memCopy(MemoryUtil.memByteBuffer(dataPointer, dataLength), hostBuffer.mapped()); + vmaFlushAllocation(allocator, hostBuffer.allocation(), 0, dataLength); + + VK10.vkCmdCopyBuffer( + commands, + hostBuffer.buffer(), + deviceBuffer.buffer(), + VkBufferCopy.calloc(1, stack) + .srcOffset(0) + .dstOffset(0) + .size(dataLength)); + + VK10.vkCmdPipelineBarrier( + commands, + VK10.VK_PIPELINE_STAGE_TRANSFER_BIT, + VK10.VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + 0, + null, + VkBufferMemoryBarrier.calloc(1, stack) + .sType$Default() + .buffer(deviceBuffer.buffer()) + .srcAccessMask(VK_ACCESS_TRANSFER_WRITE_BIT) + .dstAccessMask(VK_ACCESS_SHADER_READ_BIT) + .srcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) + .dstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) + .offset(0) + .size(dataLength), + null); + } + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/dag/ShaderDAGOperation.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/dag/ShaderDAGOperation.java new file mode 100644 index 00000000..0d2795eb --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/dag/ShaderDAGOperation.java @@ -0,0 +1,7 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel.dag; + +public interface ShaderDAGOperation { + + int getDAGId(); + +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/async/CallingThread.java b/src/main/java/com/cardinalstar/cubicchunks/async/CallingThread.java new file mode 100644 index 00000000..94b7e2cc --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/async/CallingThread.java @@ -0,0 +1,7 @@ +package com.cardinalstar.cubicchunks.async; + +public @interface CallingThread { + + ThreadType value(); + +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/async/ThreadType.java b/src/main/java/com/cardinalstar/cubicchunks/async/ThreadType.java new file mode 100644 index 00000000..34cd5ecb --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/async/ThreadType.java @@ -0,0 +1,7 @@ +package com.cardinalstar.cubicchunks.async; + +public enum ThreadType { + CLIENT, + SERVER, + WORKER +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/event/handlers/ClientEventHandler.java b/src/main/java/com/cardinalstar/cubicchunks/event/handlers/ClientEventHandler.java index 88ae8d62..ef2c43a3 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/event/handlers/ClientEventHandler.java +++ b/src/main/java/com/cardinalstar/cubicchunks/event/handlers/ClientEventHandler.java @@ -37,9 +37,9 @@ import com.cardinalstar.cubicchunks.mixin.api.ICubicWorldInternal; import com.cardinalstar.cubicchunks.mixin.early.client.IGuiOptionsRowList; import com.cardinalstar.cubicchunks.mixin.early.client.IGuiVideoSettings; -import com.cardinalstar.cubicchunks.modcompat.angelica.AngelicaInterop; import com.cardinalstar.cubicchunks.server.ICubicPlayerList; import com.cardinalstar.cubicchunks.util.MathUtil; +import com.cardinalstar.cubicchunks.util.Mods; import cpw.mods.fml.client.FMLClientHandler; import cpw.mods.fml.common.FMLCommonHandler; @@ -81,7 +81,7 @@ public void onServerTick(TickEvent.ServerTickEvent event) { public void initGuiEvent(InitGuiEvent.Post event) { GuiScreen currentGui = event.gui; - if (currentGui instanceof GuiVideoSettings && !AngelicaInterop.hasDelegate()) { + if (currentGui instanceof GuiVideoSettings && !Mods.Angelica.isModLoaded()) { GuiVideoSettings gvs = (GuiVideoSettings) currentGui; IGuiOptionsRowList gowl = (IGuiOptionsRowList) ((IGuiVideoSettings) gvs).getOptionsRowList(); GuiOptionsRowList.Row row = this.createRow(100, gvs.width); diff --git a/src/main/java/com/cardinalstar/cubicchunks/lighting/FirstLightProcessor.java b/src/main/java/com/cardinalstar/cubicchunks/lighting/FirstLightProcessor.java index ba88f37e..765f262e 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/lighting/FirstLightProcessor.java +++ b/src/main/java/com/cardinalstar/cubicchunks/lighting/FirstLightProcessor.java @@ -24,25 +24,24 @@ import static com.cardinalstar.cubicchunks.util.Coords.cubeToMaxBlock; import static com.cardinalstar.cubicchunks.util.Coords.cubeToMinBlock; -import javax.annotation.Nullable; import javax.annotation.ParametersAreNonnullByDefault; +import net.minecraft.block.Block; +import net.minecraft.init.Blocks; import net.minecraft.world.EnumSkyBlock; -import net.minecraft.world.IBlockAccess; - -import org.apache.commons.lang3.tuple.ImmutablePair; -import org.apache.commons.lang3.tuple.Pair; -import org.joml.Vector3ic; +import net.minecraft.world.World; +import net.minecraft.world.chunk.storage.ExtendedBlockStorage; import com.cardinalstar.cubicchunks.api.IColumn; import com.cardinalstar.cubicchunks.api.ICube; -import com.cardinalstar.cubicchunks.api.util.Box; +import com.cardinalstar.cubicchunks.mixin.api.BlockExt_Lighting; import com.cardinalstar.cubicchunks.mixin.api.ICubicWorldInternal; import com.cardinalstar.cubicchunks.server.chunkio.ICubeLoader; import com.cardinalstar.cubicchunks.util.MathUtil; import com.cardinalstar.cubicchunks.world.core.IColumnInternal; import com.cardinalstar.cubicchunks.world.cube.Cube; -import com.gtnewhorizon.gtnhlib.blockpos.BlockPos; + +import it.unimi.dsi.fastutil.ints.IntIntMutablePair; /** * Notes on world.checkLightFor(): Decreasing light value: Light is recalculated starting from 0 ONLY for blocks where @@ -57,12 +56,6 @@ @ParametersAreNonnullByDefault public class FirstLightProcessor { - // Iteration state data - // Cache position to avoid allocation of new object each time - private int curPosX = 0; - private int curPosY = 0; - private int curPosZ = 0; - /** * Diffuses skylight in the given cube and all cubes affected by this update. * @@ -70,39 +63,59 @@ public class FirstLightProcessor { */ public void diffuseSkylight(ICube cube) { ILightingManager lm = ((ICubicWorldInternal) cube.getWorld()).getLightingManager(); - BlockPos minPos = cube.getCoords() - .getMinBlockPos(); - BlockPos maxPos = cube.getCoords() - .getMaxBlockPos(); ICubeLoader loader = ((ICubicWorldInternal.Server) cube.getWorld()).getCubeCache() .getCubeLoader(); - loader.cacheCubes(cube.getX(), cube.getY(), cube.getZ(), 1, 1, 1); + World world = cube.getWorld(); + + int cX = cube.getX(); + int cY = cube.getY(); + int cZ = cube.getZ(); + + int bX = cX << 4; + int bY = cY << 4; + int bZ = cZ << 4; + + int bYMax = bY + 15; + + boolean isCubeEmpty = cube.isEmpty(); + + loader.cacheCubes(cX, cY, cZ, 1, 2, 1); - Box allBlocks = new Box(minPos.x, minPos.y, minPos.z, maxPos.x, maxPos.y, maxPos.z); - for (Vector3ic v : allBlocks) { - if (cube.getBlock(v.x(), v.y(), v.z()) - .getLightValue(cube.getWorld(), v.x(), v.y(), v.z()) > 0) { - lm.checkLightFor(EnumSkyBlock.Block, v.x(), v.y(), v.z()); + ExtendedBlockStorage storage = cube.getStorage(); + + if (!isCubeEmpty) { + assert storage != null; + + for (int lX = 0; lX < 16; lX++) { + for (int lY = 0; lY < 16; lY++) { + for (int lZ = 0; lZ < 16; lZ++) { + Block block = storage.getBlockByExtId(lX, lY, lZ); + + try { + ((BlockExt_Lighting) block).cc$setUnsafeLightMode(true); + + if (block != Blocks.air && block.getLightValue(world, bX + lX, bY + lY, bZ + lZ) > 0) { + lm.checkLightFor(EnumSkyBlock.Block, bX + lX, bY + lY, bZ + lZ); + } + } finally { + ((BlockExt_Lighting) block).cc$setUnsafeLightMode(false); + } + } + } } } - loader.uncacheCubes(); - if (cube.getWorld().provider.hasNoSky) { + loader.uncacheCubes(); + return; } // Cache min/max Y, generating them may be expensive - int[][] minBlockYArr = new int[Cube.SIZE][Cube.SIZE]; - int[][] maxBlockYArr = new int[Cube.SIZE][Cube.SIZE]; - - int minBlockX = cubeToMinBlock(cube.getX()); - int maxBlockX = cubeToMaxBlock(cube.getX()); - - int minBlockZ = cubeToMinBlock(cube.getZ()); - int maxBlockZ = cubeToMaxBlock(cube.getZ()); + int[] minBlockYArr = new int[Cube.SIZE * Cube.SIZE]; + int[] maxBlockYArr = new int[Cube.SIZE * Cube.SIZE]; // the lowest minHeight and the highest maxHeight values // used to make the cube iteration the outer loop, so light propagator can do mass light updates @@ -112,162 +125,148 @@ public void diffuseSkylight(ICube cube) { // Determine the block columns that require updating. If there is nothing to update, store contradicting data so // we can skip the column later. + IntIntMutablePair minMax = new IntIntMutablePair(0, 0); + IColumnInternal column = cube.getColumn(); - for (int localX = 0; localX < Cube.SIZE; ++localX) { - for (int localZ = 0; localZ < Cube.SIZE; ++localZ) { - int height = column.getTopYWithStaging(localX, localZ); - int maxY = cube.getCoords() - .getMaxBlockY(); - int maxCubeBlockY = cube.getCoords() - .getMaxBlockY(); - - int minCubeBlockX = cube.getCoords() - .getMinBlockX(); - int minCubeBlockZ = cube.getCoords() - .getMinBlockZ(); - - curPosX = minCubeBlockX + localX; - curPosY = 0; - curPosZ = minCubeBlockZ + localZ; - - int minInstantFill = Integer.MIN_VALUE, maxInstantFill = Integer.MIN_VALUE; - if (cube.getStorage() != null && localX != 0 - && localX != 15 - && localZ != 0 - && localZ != 15 - && maxCubeBlockY > height) { - int h1 = column.getTopYWithStaging(localX + 1, localZ); - int h2 = column.getTopYWithStaging(localX - 1, localZ); - int h3 = column.getTopYWithStaging(localX, localZ + 1); - int h4 = column.getTopYWithStaging(localX, localZ - 1); + + for (int lX = 0; lX < Cube.SIZE; ++lX) { + for (int lZ = 0; lZ < Cube.SIZE; ++lZ) { + // This is the top block within this block column, including the current cube. + int stagingTopBlock = column.getTopYWithStaging(lX, lZ); + + int minInstantFill = Integer.MIN_VALUE; + int maxInstantFill = Integer.MIN_VALUE; + + boolean isEdge = lX == 0 || lX == 15 || lZ == 0 || lZ == 15; + + if (!isCubeEmpty && !isEdge && bYMax > stagingTopBlock) { + int h1 = column.getTopYWithStaging(lX + 1, lZ); + int h2 = column.getTopYWithStaging(lX - 1, lZ); + int h3 = column.getTopYWithStaging(lX, lZ + 1); + int h4 = column.getTopYWithStaging(lX, lZ - 1); + int maxNeighbor = MathUtil.max(h1, h2, h3, h4) + 1; - int maxCurr = height + 2; - minInstantFill = MathUtil.max( - maxCurr, - maxNeighbor, - cube.getCoords() - .getMinBlockY()); - maxInstantFill = maxCubeBlockY; + + minInstantFill = MathUtil.max(stagingTopBlock + 2, maxNeighbor, bY); + maxInstantFill = bYMax; } - if (height < maxY) { - int minCubeBlockY = cube.getCoords() - .getMinBlockY(); - int minY = Math.max(height, minCubeBlockY); - for (int yPos = minY; yPos <= maxY; yPos++) { - curPosY = yPos; + + if (stagingTopBlock < bYMax) { + int minY = Math.max(stagingTopBlock, bY); + + for (int yPos = minY; yPos <= bYMax; yPos++) { if (yPos >= minInstantFill && yPos <= maxInstantFill) { - cube.setLightFor(EnumSkyBlock.Sky, curPosX, curPosY, curPosZ, 15); + cube.setLightFor(EnumSkyBlock.Sky, bX + lX, yPos, bZ + lZ, 15); } else { - lm.checkLightFor(EnumSkyBlock.Sky, curPosX, curPosY, curPosZ); + if (isEdge) { + cube.setLightFor(EnumSkyBlock.Sky, bX + lX, yPos, bZ + lZ, 0); + } + lm.checkLightFor(EnumSkyBlock.Sky, bX + lX, yPos, bZ + lZ); } } } - Pair minMax = getMinMaxLightUpdateY(cube, localX, localZ); - int min = minMax == null ? Integer.MAX_VALUE : minMax.getLeft(); - int max = minMax == null ? Integer.MIN_VALUE : minMax.getRight(); - minBlockYArr[localX][localZ] = min; - maxBlockYArr[localX][localZ] = max; - minMinHeight = Math.min(min, minMinHeight); - maxMaxHeight = Math.max(max, maxMaxHeight); + // If the current cube is above the highest occluding block in the column, everything is fully lit. + int cubeY = cube.getY(); + + // If the given cube lies underneath the occluding block, then the update must start at the occluding + // block. + if (cubeY < blockToCube(stagingTopBlock)) { + // This is the top block within this block column, excluding the staging height map (which means the + // blocks in this cube are ignored) + int opacityTopBlock = column.getOpacityIndex() + .getTopBlockY(lX, lZ); + + minMax.left(opacityTopBlock); + minMax.right(stagingTopBlock); + + minBlockYArr[(lZ << 4) | lX] = opacityTopBlock; + maxBlockYArr[(lZ << 4) | lX] = stagingTopBlock; + + minMinHeight = Math.min(opacityTopBlock, minMinHeight); + maxMaxHeight = Math.max(stagingTopBlock, maxMaxHeight); + } } } // Iterate over all affected cubes. - Iterable cubes = column - .getLoadedCubes(blockToCube(maxMaxHeight), blockToCube(/* minMinHeight */Integer.MIN_VALUE)); - for (ICube otherCube : cubes) { - int minCubeBlockY = otherCube.getCoords() - .getMinBlockY(); - int maxCubeBlockY = otherCube.getCoords() - .getMaxBlockY(); - for (int blockX = minBlockX; blockX <= maxBlockX; blockX++) { - for (int blockZ = minBlockZ; blockZ <= maxBlockZ; blockZ++) { - int minBlockY = minBlockYArr[blockX - minBlockX][blockZ - minBlockZ]; - int maxBlockY = maxBlockYArr[blockX - minBlockX][blockZ - minBlockZ]; - - // If no update is needed, skip the block column. + Iterable cubes = column.getLoadedCubes(blockToCube(minMinHeight), blockToCube(maxMaxHeight)); + + for (Cube affectedCube : cubes) { + int bY_Affected = affectedCube.getY() << 4; + int bYMax_Affected = bY_Affected + 15; + + for (int lX = 0; lX < 16; lX++) { + for (int lZ = 0; lZ < 16; lZ++) { + int minBlockY = minBlockYArr[(lZ << 4) | lX]; + int maxBlockY = maxBlockYArr[(lZ << 4) | lX]; + + // is below the existing top of the block if (minBlockY > maxBlockY) { continue; } + // if not in this cube, skip - if (!MathUtil.rangesIntersect(minBlockY, maxBlockY, minCubeBlockY, maxCubeBlockY)) { + if (!MathUtil.rangesIntersect(minBlockY, maxBlockY, bY_Affected, bYMax_Affected)) { continue; } - if (otherCube != cube && !otherCube.isInitialLightingDone()) { + if (affectedCube != cube && !affectedCube.isInitialLightingDone()) { continue; } - this.curPosX = blockX; - this.curPosZ = blockZ; // Update the block column in this cube. - if (!diffuseSkylightInBlockColumn( - lm, - otherCube, - this.curPosX, - this.curPosY, - this.curPosZ, - minBlockY, - maxBlockY)) { - throw new IllegalStateException( - "Check light failed at (" + this.curPosX - + ", " - + this.curPosY - + ", " - + this.curPosZ - + ")" - + "!"); - } + diffuseSkylightInBlockColumn(lm, affectedCube, lX + bX, lZ + bZ, minBlockY, maxBlockY); } } } + + loader.uncacheCubes(); } /** - * Diffuses skylight inside of the given cube in the block column specified by the given MutableBlockPos. The - * update is limited vertically by minBlockY and maxBlockY. + * Diffuses skylight inside of the given cube in the block column specified by posX/posZ. + * The update is limited vertically by minBlockY and maxBlockY. * * @param cube the cube inside of which the skylight is to be diffused * @param posX the x position of the block column to be updated - * @param posY the y position of the block column to be updated * @param posZ the z position of the block column to be updated * @param minBlockY the lower bound of the section to be updated * @param maxBlockY the upper bound of the section to be updated - * - * @return true if the update was successful, false otherwise */ - private boolean diffuseSkylightInBlockColumn(ILightingManager lm, ICube cube, int posX, int posY, int posZ, - int minBlockY, int maxBlockY) { + private void diffuseSkylightInBlockColumn(ILightingManager lm, ICube cube, int posX, int posZ, int minBlockY, + int maxBlockY) { int cubeMinBlockY = cubeToMinBlock(cube.getY()); int cubeMaxBlockY = cubeToMaxBlock(cube.getY()); int maxBlockYInCube = Math.min(cubeMaxBlockY, maxBlockY); int minBlockYInCube = Math.max(cubeMinBlockY, minBlockY); - for (int blockY = maxBlockYInCube; blockY >= minBlockYInCube; --blockY) { - posY = blockY; - if (needsSkylightUpdate(cube, posX, posY, posZ)) { - lm.checkLightFor(EnumSkyBlock.Sky, posX, posY, posZ); + ExtendedBlockStorage storage = cube.getStorage(); + + if (storage == null) { + // All air — opacity 0 < 15, always needs skylight update + for (int blockY = maxBlockYInCube; blockY >= minBlockYInCube; --blockY) { + lm.checkLightFor(EnumSkyBlock.Sky, posX, blockY, posZ); } + + return; } - return true; - } + World world = cube.getWorld(); - /** - * Determines if the block at the given position requires a skylight update. - * - * @param x the block's global x position - * @param y the block's global y position - * @param z the block's global z position - * @return true if the specified block needs a skylight update, false otherwise - */ - private static boolean needsSkylightUpdate(ICube cube, int x, int y, int z) { - // Opaque blocks don't need update. Nothing can emit skylight, and skylight can't get into them nor out of them. - IBlockAccess world = cube.getWorld(); - return cube.getBlock(x, y, z) - .getLightOpacity(world, x, y, z) < 15; + int localX = posX & 0xF; + int localZ = posZ & 0xF; + + for (int blockY = maxBlockYInCube; blockY >= minBlockYInCube; --blockY) { + // Opaque blocks don't need update. Nothing can emit skylight, and skylight can't get into them nor out of + // them. + Block block = storage.getBlockByExtId(localX, blockY & 0xF, localZ); + + if (block == Blocks.air || block.getLightOpacity(world, posX, blockY, posZ) < 15) { + lm.checkLightFor(EnumSkyBlock.Sky, posX, blockY, posZ); + } + } } /** @@ -277,11 +276,8 @@ private static boolean needsSkylightUpdate(ICube cube, int x, int y, int z) { * @param cube the cube inside of which the skylight is to be updated * @param localX the local x-coordinate of the block column * @param localZ the local z-coordinate of the block column - * - * @return a pair containing the minimum and the maximum y-coordinate to be updated in the given cube */ - @Nullable - private static ImmutablePair getMinMaxLightUpdateY(ICube cube, int localX, int localZ) { + private static boolean getMinMaxLightUpdateY(ICube cube, int localX, int localZ, IntIntMutablePair minMax) { IColumn column = cube.getColumn(); int heightMax = ((IColumnInternal) column).getTopYWithStaging(localX, localZ);// ==Y of the top block @@ -289,18 +285,16 @@ private static ImmutablePair getMinMaxLightUpdateY(ICube cube, // If the given cube is above the highest occluding block in the column, everything is fully lit. int cubeY = cube.getY(); if (blockToCube(heightMax) < cubeY) { - return null; + return false; } - // If the given cube lies underneath the occluding block, - // then only blocks in this cube need updating, already handled - // if (cubeY < blockToCube(heightMax)) { - // return null; - // } - // ... otherwise, the update must start at the occluding block. + // If the given cube lies underneath the occluding block, then the update must start at the occluding block. int previousMaxHeight = column.getOpacityIndex() .getTopBlockY(localX, localZ); - // noinspection SuspiciousNameCombination - return new ImmutablePair<>(previousMaxHeight, heightMax); + + minMax.left(previousMaxHeight); + minMax.right(heightMax); + + return true; } } diff --git a/src/main/java/com/cardinalstar/cubicchunks/lighting/LightingManager.java b/src/main/java/com/cardinalstar/cubicchunks/lighting/LightingManager.java index cd40f455..f1a27f88 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/lighting/LightingManager.java +++ b/src/main/java/com/cardinalstar/cubicchunks/lighting/LightingManager.java @@ -45,7 +45,6 @@ import com.cardinalstar.cubicchunks.util.Coords; import com.cardinalstar.cubicchunks.world.core.IColumnInternal; import com.cardinalstar.cubicchunks.world.cube.Cube; -import com.gtnewhorizon.gtnhlib.blockpos.BlockPos; // TODO: extract interfaces when it's done @ParametersAreNonnullByDefault @@ -185,16 +184,17 @@ private CubicPlayerManager getPlayerManager() { @Override public void onTrackCubeSurface(ICube cube) { if (!world.isRemote) { - BlockPos min = cube.getCoords() - .getMinBlockPos(); - BlockPos max = cube.getCoords() - .getMaxBlockPos(); - for (BlockPos pos : BlockPos - .getAllInBox(min.getX(), min.getY(), min.getZ(), max.getX(), max.getY(), max.getZ())) { - - CubicPlayerManager playerManager = getPlayerManager(); - - if (playerManager != null) playerManager.heightUpdated(pos.getX(), pos.getZ()); + CubicPlayerManager playerManager = getPlayerManager(); + if (playerManager != null) { + int minX = cube.getCoords() + .getMinBlockX(); + int minZ = cube.getCoords() + .getMinBlockZ(); + for (int dx = 0; dx < 16; dx++) { + for (int dz = 0; dz < 16; dz++) { + playerManager.heightUpdated(minX + dx, minZ + dz); + } + } } tryScheduleOnLoadHeightChangeRelight(cube); } diff --git a/src/main/java/com/cardinalstar/cubicchunks/lighting/phosphor/LightingHooks.java b/src/main/java/com/cardinalstar/cubicchunks/lighting/phosphor/LightingHooks.java index 458075a2..3407e8e9 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/lighting/phosphor/LightingHooks.java +++ b/src/main/java/com/cardinalstar/cubicchunks/lighting/phosphor/LightingHooks.java @@ -53,14 +53,7 @@ public static void scheduleRelightChecksForArea(final World world, final EnumSky private static void scheduleRelightChecksForColumn(final World world, final EnumSkyBlock lightType, final int x, final int z, final int yMin, final int yMax) { - scheduleRelightChecksForColumn( - world, - world.getChunkFromBlockCoords(blockToCube(x), blockToCube(z)), - lightType, - x, - z, - yMin, - yMax); + scheduleRelightChecksForColumn(world, world.getChunkFromBlockCoords(x, z), lightType, x, z, yMin, yMax); } private static void scheduleRelightChecksForColumn(final World world, final Chunk chunk, diff --git a/src/main/java/com/cardinalstar/cubicchunks/mixin/Mixins.java b/src/main/java/com/cardinalstar/cubicchunks/mixin/Mixins.java index b199a590..a4759101 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/mixin/Mixins.java +++ b/src/main/java/com/cardinalstar/cubicchunks/mixin/Mixins.java @@ -76,12 +76,6 @@ public enum Mixins implements IMixins { .addCommonMixins("common.worldgen.MixinChunkProviderGenerate") .setPhase(Phase.EARLY) .setApplyIf(() -> true)), - MIXIN_EBS(new MixinBuilder("Add simple cache to ExtendedBlockStorage.getBlockByExtId") - .addCommonMixins("common.MixinExtendedBlockStorage") - .setPhase(Phase.EARLY) - .addExcludedMod(Mods.NotEnoughIDs) - .addExcludedMod(Mods.ChunkAPI) - .setApplyIf(() -> true)), ACCESSOR_S23(new MixinBuilder("Accessors for X/Y/Z fields for S23PacketBlockChange") .addCommonMixins("common.AccessorS23PacketBlockChange") .setPhase(Phase.EARLY) @@ -100,6 +94,24 @@ public enum Mixins implements IMixins { .addCommonMixins("common.MixinWorld_DeferInit", "common.MixinWorld_DeferInit$MixinWorldServer") .setPhase(Phase.EARLY) .setApplyIf(() -> true)), + ACCESSOR_NBT(new MixinBuilder("Add accessors for NBT tag internals.") + .addCommonMixins("common.AccessorNBTTagList", "common.AccessorNBTTagCompound") + .setPhase(Phase.EARLY) + .setApplyIf(() -> true)), + UNSAFE_LIGHTING(new MixinBuilder("Disable some pointless operations while performing light checks") + .addCommonMixins("common.MixinBlock_Lighting") + .setPhase(Phase.EARLY) + .setApplyIf(() -> true)), + EBS_ID_ACCEL_VANILLA(new MixinBuilder("Adds hooks for updating EBS block ID without block lookup (vanilla-only)") + .addCommonMixins("common.MixinEBSID_Vanilla") + .setPhase(Phase.EARLY) + .addExcludedMod(Mods.EndlessIDs) + .setApplyIf(() -> true)), + EBS_ID_ACCEL_EID(new MixinBuilder("Adds hooks for updating EBS block ID without block lookup (endless ids-only)") + .addCommonMixins("common.MixinEBSID_EID") + .setPhase(Phase.EARLY) + .addRequiredMod(Mods.EndlessIDs) + .setApplyIf(() -> true)), // CHUNK MIXIN_CHUNK(new MixinBuilder("Various modifications to inject cubes, height map patches, etc into Chunks.") @@ -311,7 +323,13 @@ public enum Mixins implements IMixins { .addCommonMixins("mod.MixinBlockPosUtil") .setPhase(Phase.LATE) .addRequiredMod(Mods.ChunkAPI) - .setApplyIf(() -> true)) + .setApplyIf(() -> true)), + MIXIN_COORD_PACKER_CELERITAS( + new MixinBuilder("Overwrite Angelica/Celeritas PositionUtil packing methods with CC-compatible ones") + .addCommonMixins("mod.MixinPositionUtil") + .setPhase(Phase.EARLY) + .addRequiredMod(Mods.Angelica) + .setApplyIf(() -> true)), // ; diff --git a/src/main/java/com/cardinalstar/cubicchunks/mixin/api/BlockExt_Lighting.java b/src/main/java/com/cardinalstar/cubicchunks/mixin/api/BlockExt_Lighting.java new file mode 100644 index 00000000..c0bc8e1f --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/mixin/api/BlockExt_Lighting.java @@ -0,0 +1,7 @@ +package com.cardinalstar.cubicchunks.mixin.api; + +public interface BlockExt_Lighting { + + void cc$setUnsafeLightMode(boolean enable); + +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/AccessorNBTTagCompound.java b/src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/AccessorNBTTagCompound.java new file mode 100644 index 00000000..79b27023 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/AccessorNBTTagCompound.java @@ -0,0 +1,17 @@ +package com.cardinalstar.cubicchunks.mixin.early.common; + +import java.util.Map; + +import net.minecraft.nbt.NBTBase; +import net.minecraft.nbt.NBTTagCompound; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(NBTTagCompound.class) +public interface AccessorNBTTagCompound { + + @Accessor("tagMap") + Map getTagMap(); + +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/AccessorNBTTagList.java b/src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/AccessorNBTTagList.java new file mode 100644 index 00000000..b520c508 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/AccessorNBTTagList.java @@ -0,0 +1,17 @@ +package com.cardinalstar.cubicchunks.mixin.early.common; + +import java.util.List; + +import net.minecraft.nbt.NBTBase; +import net.minecraft.nbt.NBTTagList; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(NBTTagList.class) +public interface AccessorNBTTagList { + + @Accessor("tagList") + List getTagList(); + +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/MixinBlock_Lighting.java b/src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/MixinBlock_Lighting.java new file mode 100644 index 00000000..8d02ed9b --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/MixinBlock_Lighting.java @@ -0,0 +1,32 @@ +package com.cardinalstar.cubicchunks.mixin.early.common; + +import net.minecraft.block.Block; +import net.minecraft.world.IBlockAccess; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import com.cardinalstar.cubicchunks.mixin.api.BlockExt_Lighting; + +@Mixin(Block.class) +public class MixinBlock_Lighting implements BlockExt_Lighting { + + @Unique + private boolean cc$unsafeLightMode = false; + + @Override + public void cc$setUnsafeLightMode(boolean enable) { + cc$unsafeLightMode = enable; + } + + @Redirect( + method = "getLightValue(Lnet/minecraft/world/IBlockAccess;III)I", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/IBlockAccess;getBlock(III)Lnet/minecraft/block/Block;")) + public Block cc$noopGetBlock(IBlockAccess instance, int x, int y, int z) { + return cc$unsafeLightMode ? (Block) (Object) this : instance.getBlock(x, y, z); + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/MixinEBSID_EID.java b/src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/MixinEBSID_EID.java new file mode 100644 index 00000000..c0e94f7c --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/MixinEBSID_EID.java @@ -0,0 +1,43 @@ +package com.cardinalstar.cubicchunks.mixin.early.common; + +import net.minecraft.world.chunk.storage.ExtendedBlockStorage; + +import org.spongepowered.asm.mixin.Dynamic; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import com.cardinalstar.cubicchunks.mixin.ext.EBSIDAccessor; + +@Mixin(ExtendedBlockStorage.class) +public abstract class MixinEBSID_EID implements EBSIDAccessor { + + @Dynamic + public abstract int eid$getID(int x, int y, int z); + + @Dynamic + public abstract void eid$setID(int x, int y, int z, int id); + + @Shadow + private int tickRefCount; + + @Shadow + private int blockRefCount; + + @Override + public int getBlockID(int x, int y, int z) { + return eid$getID(x, y, z); + } + + @Override + public void setBlockID(int x, int y, int z, int id, boolean tickRandomly) { + eid$setID(x, y, z, id); + + if (tickRandomly) { + tickRefCount++; + } + + if (id != 0) { + blockRefCount++; + } + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/MixinEBSID_Vanilla.java b/src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/MixinEBSID_Vanilla.java new file mode 100644 index 00000000..7f186904 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/MixinEBSID_Vanilla.java @@ -0,0 +1,59 @@ +package com.cardinalstar.cubicchunks.mixin.early.common; + +import net.minecraft.world.chunk.NibbleArray; +import net.minecraft.world.chunk.storage.ExtendedBlockStorage; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import com.cardinalstar.cubicchunks.mixin.ext.EBSIDAccessor; + +@Mixin(ExtendedBlockStorage.class) +public class MixinEBSID_Vanilla implements EBSIDAccessor { + + @Shadow + private byte[] blockLSBArray; + + @Shadow + private NibbleArray blockMSBArray; + + @Shadow + private int tickRefCount; + + @Shadow + private int blockRefCount; + + @Override + public int getBlockID(int x, int y, int z) { + int id = this.blockLSBArray[y << 8 | z << 4 | x] & 255; + + if (this.blockMSBArray != null) { + id |= this.blockMSBArray.get(x, y, z) << 8; + } + + return id; + } + + @Override + public void setBlockID(int x, int y, int z, int id, boolean tickRandomly) { + this.blockLSBArray[y << 8 | z << 4 | x] = (byte) (id & 255); + + if (id > 255) { + if (this.blockMSBArray == null) { + this.blockMSBArray = new NibbleArray(this.blockLSBArray.length, 4); + } + + this.blockMSBArray.set(x, y, z, (id & 3840) >> 8); + } else if (this.blockMSBArray != null) { + this.blockMSBArray.set(x, y, z, 0); + } + + if (tickRandomly) { + tickRefCount++; + } + + if (id != 0) { + blockRefCount++; + } + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/MixinExtendedBlockStorage.java b/src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/MixinExtendedBlockStorage.java deleted file mode 100644 index 719c58d8..00000000 --- a/src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/MixinExtendedBlockStorage.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.cardinalstar.cubicchunks.mixin.early.common; - -import net.minecraft.block.Block; -import net.minecraft.init.Blocks; -import net.minecraft.world.chunk.storage.ExtendedBlockStorage; - -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Unique; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Redirect; - -@Mixin(ExtendedBlockStorage.class) -public class MixinExtendedBlockStorage { - - @Unique - private Block prevBlock = Blocks.air; - @Unique - private int prevId = 0; - - @Redirect( - method = "getBlockByExtId", - at = @At(value = "INVOKE", target = "Lnet/minecraft/block/Block;getBlockById(I)Lnet/minecraft/block/Block;")) - public final Block optimizeGetBlock(int id) { - if (id == prevId) return prevBlock; - - prevId = id; - return prevBlock = Block.getBlockById(id); - } -} diff --git a/src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/MixinWorld.java b/src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/MixinWorld.java index 01d5cb94..39547caf 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/MixinWorld.java +++ b/src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/MixinWorld.java @@ -25,6 +25,7 @@ import java.util.List; import java.util.Random; +import java.util.Set; import java.util.function.Predicate; import javax.annotation.Nullable; @@ -37,6 +38,7 @@ import net.minecraft.profiler.Profiler; import net.minecraft.tileentity.TileEntity; import net.minecraft.util.AxisAlignedBB; +import net.minecraft.world.ChunkCoordIntPair; import net.minecraft.world.EnumSkyBlock; import net.minecraft.world.GameRules; import net.minecraft.world.IBlockAccess; @@ -87,6 +89,8 @@ import com.cardinalstar.cubicchunks.world.cube.ICubeProviderInternal; import com.google.common.collect.ImmutableList; import com.gtnewhorizon.gtnhlib.blockpos.BlockPos; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; import com.llamalad7.mixinextras.sugar.Local; /** @@ -516,4 +520,15 @@ private int collidingBoxFix2(int constant, @Local(argsOnly = true) AxisAlignedBB private boolean shouldSkipWorld(World world) { return !allowedServerWorldClasses.contains(world.getClass()); } + + @WrapOperation( + method = "setActivePlayerChunksAndCheckLight", + at = @At(value = "INVOKE", target = "Ljava/util/Set;add(Ljava/lang/Object;)Z")) + public boolean skipUnloadedChunks(Set instance, Object coord2, Operation original) { + ChunkCoordIntPair coord = (ChunkCoordIntPair) coord2; + + if (!this.chunkExists(coord.chunkXPos, coord.chunkZPos)) return false; + + return instance.add(coord); + } } diff --git a/src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/MixinWorldProvider.java b/src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/MixinWorldProvider.java index 02994d2f..e41c51a4 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/MixinWorldProvider.java +++ b/src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/MixinWorldProvider.java @@ -103,6 +103,10 @@ public IWorldGenerator createCubeGenerator() { return ccWorldType.createCubeGenerator(worldObj); } + if (worldObj.provider instanceof com.cardinalstar.cubicchunks.api.world.ICubicWorldProvider cubicWorldProvider) { + return cubicWorldProvider.createWorldGenerator(worldObj); + } + return new VanillaWorldGenerator( worldObj.provider.createChunkGenerator(), worldObj, diff --git a/src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/MixinWorld_HeightLimit.java b/src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/MixinWorld_HeightLimit.java index 033544a7..72a93793 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/MixinWorld_HeightLimit.java +++ b/src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/MixinWorld_HeightLimit.java @@ -26,14 +26,17 @@ import javax.annotation.ParametersAreNonnullByDefault; import net.minecraft.block.Block; +import net.minecraft.client.multiplayer.WorldClient; import net.minecraft.entity.Entity; import net.minecraft.world.World; import net.minecraft.world.WorldProvider; +import net.minecraft.world.WorldServer; import net.minecraft.world.chunk.Chunk; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Constant; import org.spongepowered.asm.mixin.injection.Group; @@ -59,6 +62,15 @@ @Mixin(World.class) public abstract class MixinWorld_HeightLimit implements ICubicWorld { + @Unique + private boolean cc$isCubicWorld = isCubic(); + + @Unique + private boolean isCubic() { + //noinspection ConstantValue + return ((Object) this) instanceof WorldServer || ((Object) this) instanceof WorldClient; + } + @Shadow public int skylightSubtracted; @@ -116,9 +128,13 @@ private int getBlock_heightLimits_max(int original) { @Expression("this.chunkExists(?, ?)") @Redirect(method = "blockExists", at = @At("MIXINEXTRAS:EXPRESSION")) boolean redirectChunkExistsCubeExists(World instance, int p_72916_1_, int p_72916_2_, - @Local(argsOnly = true, ordinal = 0) int x, @Local(argsOnly = true, ordinal = 1) int y, - @Local(argsOnly = true, ordinal = 2) int z) { - return cubeExists(x >> 4, y >> 4, z >> 4); + @Local(argsOnly = true, name = "p_72899_1_") int x, @Local(argsOnly = true, name = "p_72899_2_") int y, + @Local(argsOnly = true, name = "p_72899_3_") int z) { + if (cc$isCubicWorld) { + return cubeExists(x >> 4, y >> 4, z >> 4); + } else { + return chunkExists(x >> 4, z >> 4); + } } // checkChunksExist diff --git a/src/main/java/com/cardinalstar/cubicchunks/mixin/early/mod/MixinPositionUtil.java b/src/main/java/com/cardinalstar/cubicchunks/mixin/early/mod/MixinPositionUtil.java new file mode 100644 index 00000000..55b0e97f --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/mixin/early/mod/MixinPositionUtil.java @@ -0,0 +1,56 @@ +package com.cardinalstar.cubicchunks.mixin.early.mod; + +import org.embeddedt.embeddium.impl.util.PositionUtil; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; + +import com.cardinalstar.cubicchunks.util.Coords; + +@Mixin(value = PositionUtil.class, remap = false) +public class MixinPositionUtil { + + /** + * @author Recursive Pineapple + * @reason Performance + */ + @Overwrite + public static long packBlock(int x, int y, int z) { + return Coords.key(x, y, z); + } + + /** + * @author Recursive Pineapple + * @reason Performance + */ + @Overwrite + public static int unpackBlockX(long packed) { + return Coords.x(packed); + } + + /** + * @author Recursive Pineapple + * @reason Performance + */ + @Overwrite + public static int unpackBlockY(long packed) { + return Coords.y(packed); + } + + /** + * @author Recursive Pineapple + * @reason Performance + */ + @Overwrite + public static int unpackBlockZ(long packed) { + return Coords.z(packed); + } + + /** + * @author Recursive Pineapple + * @reason Performance + */ + @Overwrite + public static long packSection(int x, int y, int z) { + return Coords.key(x, y, z); + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/mixin/ext/EBSIDAccessor.java b/src/main/java/com/cardinalstar/cubicchunks/mixin/ext/EBSIDAccessor.java new file mode 100644 index 00000000..d5e34aa7 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/mixin/ext/EBSIDAccessor.java @@ -0,0 +1,9 @@ +package com.cardinalstar.cubicchunks.mixin.ext; + +public interface EBSIDAccessor { + + int getBlockID(int x, int y, int z); + + void setBlockID(int x, int y, int z, int id, boolean tickRandomly); + +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/modcompat/angelica/AngelicaInterop.java b/src/main/java/com/cardinalstar/cubicchunks/modcompat/angelica/AngelicaInterop.java deleted file mode 100644 index 713d279f..00000000 --- a/src/main/java/com/cardinalstar/cubicchunks/modcompat/angelica/AngelicaInterop.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.cardinalstar.cubicchunks.modcompat.angelica; - -public class AngelicaInterop { - - private static IAngelicaDelegate delegate; - - public static boolean hasDelegate() { - return delegate != null; - } - - public static IAngelicaDelegate getDelegate() { - return delegate; - } - - public static void setDelegate(IAngelicaDelegate delegate) { - AngelicaInterop.delegate = delegate; - } -} diff --git a/src/main/java/com/cardinalstar/cubicchunks/modcompat/angelica/IAngelicaDelegate.java b/src/main/java/com/cardinalstar/cubicchunks/modcompat/angelica/IAngelicaDelegate.java deleted file mode 100644 index 44d85ce0..00000000 --- a/src/main/java/com/cardinalstar/cubicchunks/modcompat/angelica/IAngelicaDelegate.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.cardinalstar.cubicchunks.modcompat.angelica; - -public interface IAngelicaDelegate { - - void onColumnLoaded(int chunkX, int chunkZ); - - void onColumnUnloaded(int chunkX, int chunkZ); - - void onCubeLoaded(int cubeX, int cubeY, int cubeZ); - - void onCubeUnloaded(int cubeX, int cubeY, int cubeZ); - -} diff --git a/src/main/java/com/cardinalstar/cubicchunks/network/CCPacketEntry.java b/src/main/java/com/cardinalstar/cubicchunks/network/CCPacketEntry.java index 86c8bbc0..a358e07c 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/network/CCPacketEntry.java +++ b/src/main/java/com/cardinalstar/cubicchunks/network/CCPacketEntry.java @@ -10,6 +10,7 @@ public enum CCPacketEntry { HeightMapUpdate(new PacketEncoderHeightMapUpdate()), CubeSkyLightUpdates(new PacketEncoderCubeSkyLightUpdates()), WorldHeight(new PacketEncoderWorldHeight()), + UpdateVisualizedBoxes(new PacketEncoderUpdateVisualizedBoxes()), // ; diff --git a/src/main/java/com/cardinalstar/cubicchunks/network/NetworkChannel.java b/src/main/java/com/cardinalstar/cubicchunks/network/NetworkChannel.java index 07b32a18..b2eed1b3 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/network/NetworkChannel.java +++ b/src/main/java/com/cardinalstar/cubicchunks/network/NetworkChannel.java @@ -69,14 +69,19 @@ protected void encode(ChannelHandlerContext context, CCPacket packet, List encoder = this.encoders[packet.getPacketID()]; - encoder.writePacket(new CCPacketBuffer(buffer), packet); - - output.add( - new FMLProxyPacket( - buffer, - context.channel() - .attr(NetworkRegistry.FML_CHANNEL) - .get())); + + try { + encoder.writePacket(new CCPacketBuffer(buffer), packet); + + output.add( + new FMLProxyPacket( + buffer, + context.channel() + .attr(NetworkRegistry.FML_CHANNEL) + .get())); + } catch (Throwable t) { + CubicChunks.LOGGER.error("Could not write packet: {}", packet, t); + } } @Override @@ -86,9 +91,14 @@ protected void decode(ChannelHandlerContext context, FMLProxyPacket proxyPacket, buffer = proxyPacket.payload(); CCPacketEncoder encoder = this.encoders[buffer.readByte()]; - CCPacket packet = encoder.readPacket(new CCPacketBuffer(buffer)); - encoder.setINetHandler(proxyPacket.handler(), packet); - output.add(packet); + + try { + CCPacket packet = encoder.readPacket(new CCPacketBuffer(buffer)); + encoder.setINetHandler(proxyPacket.handler(), packet); + output.add(packet); + } catch (Throwable t) { + CubicChunks.LOGGER.error("Could not read packet: {}", encoder, t); + } } public void sendToPlayer(CCPacket packet, EntityPlayerMP player) { diff --git a/src/main/java/com/cardinalstar/cubicchunks/network/PacketEncoderColumn.java b/src/main/java/com/cardinalstar/cubicchunks/network/PacketEncoderColumn.java index b9ea141f..6bb040ee 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/network/PacketEncoderColumn.java +++ b/src/main/java/com/cardinalstar/cubicchunks/network/PacketEncoderColumn.java @@ -26,7 +26,6 @@ import net.minecraft.world.chunk.Chunk; import com.cardinalstar.cubicchunks.client.CubeProviderClient; -import com.cardinalstar.cubicchunks.modcompat.angelica.AngelicaInterop; import com.cardinalstar.cubicchunks.world.ICubicWorld; import com.github.bsideup.jabel.Desugar; @@ -84,10 +83,5 @@ public void process(World world, PacketColumn packet) { WorldEncoder.decodeColumn(new CCPacketBuffer(buf), column); }); - - if (AngelicaInterop.hasDelegate()) { - AngelicaInterop.getDelegate() - .onColumnLoaded(packet.chunkX, packet.chunkZ); - } } } diff --git a/src/main/java/com/cardinalstar/cubicchunks/network/PacketEncoderCube.java b/src/main/java/com/cardinalstar/cubicchunks/network/PacketEncoderCube.java index ae2dfb71..670ccae1 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/network/PacketEncoderCube.java +++ b/src/main/java/com/cardinalstar/cubicchunks/network/PacketEncoderCube.java @@ -25,13 +25,13 @@ import javax.annotation.ParametersAreNonnullByDefault; +import net.jpountz.lz4.LZ4Factory; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.tileentity.TileEntity; import net.minecraft.world.World; import com.cardinalstar.cubicchunks.CubicChunks; import com.cardinalstar.cubicchunks.client.CubeProviderClient; -import com.cardinalstar.cubicchunks.modcompat.angelica.AngelicaInterop; import com.cardinalstar.cubicchunks.network.PacketEncoderCube.PacketCube; import com.cardinalstar.cubicchunks.util.CubePos; import com.cardinalstar.cubicchunks.util.CubeStatusVisualizer; @@ -91,7 +91,11 @@ public byte getPacketID() { public void writePacket(CCPacketBuffer buffer, PacketCube packet) { buffer.writeCubePos(packet.cubePos); - buffer.writeByteArray(packet.data); + buffer.writeVarIntToBuffer(packet.data.length); + buffer.writeByteArray( + LZ4Factory.fastestInstance() + .fastCompressor() + .compress(packet.data)); buffer.writeList(packet.tileEntityTags, CCPacketBuffer::writeCompoundTag); } @@ -100,11 +104,16 @@ public void writePacket(CCPacketBuffer buffer, PacketCube packet) { public PacketCube readPacket(CCPacketBuffer buf) { CubePos pos = buf.readCubePos(); + byte[] decompressed = new byte[buf.readVarIntFromBuffer()]; byte[] data = buf.readByteArray(); + LZ4Factory.fastestInstance() + .fastDecompressor() + .decompress(data, decompressed); + List tileEntityTags = buf.readList(CCPacketBuffer::readCompoundTag); - return new PacketCube(pos, data, tileEntityTags); + return new PacketCube(pos, decompressed, tileEntityTags); } @Override @@ -125,11 +134,6 @@ public void process(World world, PacketCube packet) { cube.markForRenderUpdate(); - if (AngelicaInterop.hasDelegate()) { - AngelicaInterop.getDelegate() - .onCubeLoaded(cube.getX(), cube.getY(), cube.getZ()); - } - for (var tag : packet.tileEntityTags) { int blockX = tag.getInteger("x"); int blockY = tag.getInteger("y"); diff --git a/src/main/java/com/cardinalstar/cubicchunks/network/PacketEncoderUnloadColumn.java b/src/main/java/com/cardinalstar/cubicchunks/network/PacketEncoderUnloadColumn.java index 7067d9f9..bff4414f 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/network/PacketEncoderUnloadColumn.java +++ b/src/main/java/com/cardinalstar/cubicchunks/network/PacketEncoderUnloadColumn.java @@ -25,7 +25,6 @@ import net.minecraft.world.World; import com.cardinalstar.cubicchunks.client.CubeProviderClient; -import com.cardinalstar.cubicchunks.modcompat.angelica.AngelicaInterop; import com.cardinalstar.cubicchunks.world.ICubicWorld; import com.github.bsideup.jabel.Desugar; @@ -69,10 +68,5 @@ public void process(World world, PacketUnloadColumn packet) { CubeProviderClient cubeCache = (CubeProviderClient) worldClient.getCubeCache(); cubeCache.unloadChunk(packet.chunkX, packet.chunkZ); - - if (AngelicaInterop.hasDelegate()) { - AngelicaInterop.getDelegate() - .onColumnUnloaded(packet.chunkX, packet.chunkZ); - } } } diff --git a/src/main/java/com/cardinalstar/cubicchunks/network/PacketEncoderUnloadCube.java b/src/main/java/com/cardinalstar/cubicchunks/network/PacketEncoderUnloadCube.java index 2aac913e..9e7bcf47 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/network/PacketEncoderUnloadCube.java +++ b/src/main/java/com/cardinalstar/cubicchunks/network/PacketEncoderUnloadCube.java @@ -25,7 +25,6 @@ import net.minecraft.world.World; import com.cardinalstar.cubicchunks.client.CubeProviderClient; -import com.cardinalstar.cubicchunks.modcompat.angelica.AngelicaInterop; import com.cardinalstar.cubicchunks.util.CubePos; import com.cardinalstar.cubicchunks.world.ICubicWorld; import com.github.bsideup.jabel.Desugar; @@ -72,10 +71,5 @@ public void process(World world, PacketUnloadCube packet) { cubeCache.getCube(packet.pos) .markForRenderUpdate(); cubeCache.unloadCube(packet.pos); - - if (AngelicaInterop.hasDelegate()) { - AngelicaInterop.getDelegate() - .onCubeUnloaded(packet.pos.getX(), packet.pos.getY(), packet.pos.getZ()); - } } } diff --git a/src/main/java/com/cardinalstar/cubicchunks/network/PacketEncoderUpdateVisualizedBoxes.java b/src/main/java/com/cardinalstar/cubicchunks/network/PacketEncoderUpdateVisualizedBoxes.java new file mode 100644 index 00000000..26a48caf --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/network/PacketEncoderUpdateVisualizedBoxes.java @@ -0,0 +1,75 @@ +package com.cardinalstar.cubicchunks.network; + +import java.util.List; + +import net.minecraft.util.AxisAlignedBB; +import net.minecraft.world.World; + +import com.cardinalstar.cubicchunks.network.PacketEncoderUpdateVisualizedBoxes.PacketUpdateVisualizedBoxes; +import com.cardinalstar.cubicchunks.util.boxvisualizer.VisualizedBox; +import com.cardinalstar.cubicchunks.util.boxvisualizer.VisualizedBoxRenderer; +import com.github.bsideup.jabel.Desugar; +import com.gtnewhorizon.gtnhlib.color.RGBColor; + +public class PacketEncoderUpdateVisualizedBoxes extends CCPacketEncoder { + + @Desugar + public record PacketUpdateVisualizedBoxes(long timeout, boolean append, boolean disableDepth, + List boxes) implements CCPacket { + + @Override + public byte getPacketID() { + return CCPacketEntry.UpdateVisualizedBoxes.id; + } + } + + @Override + public byte getPacketID() { + return CCPacketEntry.UpdateVisualizedBoxes.id; + } + + @Override + public void writePacket(CCPacketBuffer buffer, PacketUpdateVisualizedBoxes packet) { + buffer.writeLong(packet.timeout); + buffer.writeBoolean(packet.append); + buffer.writeBoolean(packet.disableDepth); + + buffer.writeList(packet.boxes, ($, value) -> { + buffer.writeInt(value.color.toIntRGBA()); + + buffer.writeDouble(value.bounds.minX); + buffer.writeDouble(value.bounds.minY); + buffer.writeDouble(value.bounds.minZ); + buffer.writeDouble(value.bounds.maxX); + buffer.writeDouble(value.bounds.maxY); + buffer.writeDouble(value.bounds.maxZ); + }); + } + + @Override + public PacketUpdateVisualizedBoxes readPacket(CCPacketBuffer buffer) { + long timeout = buffer.readLong(); + boolean append = buffer.readBoolean(); + boolean disableDepth = buffer.readBoolean(); + + var boxes = buffer.readList($ -> { + RGBColor color = RGBColor.fromRGBA(buffer.readInt()); + + double minX = buffer.readDouble(); + double minY = buffer.readDouble(); + double minZ = buffer.readDouble(); + double maxX = buffer.readDouble(); + double maxY = buffer.readDouble(); + double maxZ = buffer.readDouble(); + + return new VisualizedBox(color, AxisAlignedBB.getBoundingBox(minX, minY, minZ, maxX, maxY, maxZ)); + }); + + return new PacketUpdateVisualizedBoxes(timeout, append, disableDepth, boxes); + } + + @Override + public void process(World world, PacketUpdateVisualizedBoxes packet) { + VisualizedBoxRenderer.receiveBoxes(packet.timeout, packet.append, packet.boxes, packet.disableDepth); + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/server/CubeProviderServer.java b/src/main/java/com/cardinalstar/cubicchunks/server/CubeProviderServer.java index dac9c6a6..b0ca6371 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/server/CubeProviderServer.java +++ b/src/main/java/com/cardinalstar/cubicchunks/server/CubeProviderServer.java @@ -51,11 +51,17 @@ import com.cardinalstar.cubicchunks.api.ICube; import com.cardinalstar.cubicchunks.api.XYZAddressable; import com.cardinalstar.cubicchunks.api.worldgen.IWorldGenerator; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.AcceleratableWorldGenerator; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.ComputePlan; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelContext; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelScheduler; import com.cardinalstar.cubicchunks.server.chunkio.CubeInitLevel; import com.cardinalstar.cubicchunks.server.chunkio.CubeLoaderCallback; import com.cardinalstar.cubicchunks.server.chunkio.CubeLoaderServer; import com.cardinalstar.cubicchunks.server.chunkio.ICubeLoader; import com.cardinalstar.cubicchunks.util.CubePos; +import com.cardinalstar.cubicchunks.util.CubeStatusVisualizer; +import com.cardinalstar.cubicchunks.util.CubeStatusVisualizer.CubeStatus; import com.cardinalstar.cubicchunks.util.XZAddressable; import com.cardinalstar.cubicchunks.world.api.ICubeProviderServer; import com.cardinalstar.cubicchunks.world.column.EmptyColumn; @@ -67,6 +73,7 @@ import com.google.common.collect.MultimapBuilder; import it.unimi.dsi.fastutil.ints.Int2ObjectRBTreeMap; +import it.unimi.dsi.fastutil.ints.IntArrayList; import it.unimi.dsi.fastutil.ints.IntComparator; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet; @@ -105,7 +112,7 @@ public class CubeProviderServer extends ChunkProviderServer private final Map eagerLoads = new Object2ObjectOpenHashMap<>(); private final List eagerLoadOrder = new ArrayList<>(); - private static final int MAX_NS_SPENT_LOADING = 10_000_000; + private static final int MAX_NS_SPENT_LOADING = 40_000_000; private int loadedColumns, loadedCubes; private long lastTickEnd = 0, loadTimeAccumulator; @@ -309,10 +316,6 @@ public void removeCallback(CubeLoaderCallback callback) { public void tick() { getCubeLoader().setNow(worldObj.getTotalWorldTime()); - doEagerLoading(); - } - - private void doEagerLoading() { profiler.startSection("Eager object sorting"); // TODO: make this faster @@ -320,7 +323,33 @@ private void doEagerLoading() { Comparator.comparingInt(this::getChunkDistanceSquared) .reversed()); - profiler.endStartSection("Eager object loading"); + profiler.endSection(); + + long pre = System.nanoTime(); + + if (worldGenerator instanceof AcceleratableWorldGenerator && KernelContext.isEnabled()) { + doAcceleratedTerrainGeneration(); + } + + doEagerLoading(); + + long post = System.nanoTime(); + + acc += post - pre; + + if (counter++ == 20) { + CubicChunks.LOGGER.info("Worldgen {}ms", (acc / 1e6) / counter); + + acc = 0; + counter = 0; + } + } + + private long acc; + private int counter; + + private void doEagerLoading() { + profiler.startSection("Eager object loading"); long start = System.nanoTime(); @@ -346,6 +375,7 @@ private void doEagerLoading() { cubeIter.remove(); request.completed = true; + CubeStatusVisualizer.remove(request.pos, CubeStatus.Enqueued); processed++; @@ -420,6 +450,60 @@ private void doEagerLoading() { profiler.endSection(); } + private void doAcceleratedTerrainGeneration() { + profiler.startSection("Accelerated terrain generation"); + + AcceleratableWorldGenerator generator = (AcceleratableWorldGenerator) this.worldGenerator; + KernelScheduler scheduler = KernelContext.getScheduler(); + + long start = System.nanoTime(); + long gpuEstimatedNs = 0; + + int i = eagerLoadOrder.size() - 1; + + List plans = new ArrayList<>(); + + while (i >= 0) { + if ((System.nanoTime() - start) >= MAX_NS_SPENT_LOADING) break; + if (gpuEstimatedNs >= scheduler.getGpuBudget()) break; + + ChunkCoordIntPair coord = eagerLoadOrder.get(i--); + + EagerCubeLoadContainer container = eagerLoads.get(coord); + + if (container == null) { + continue; + } + + IntArrayList toGenerate = new IntArrayList(); + + for (var cubeRequest : container.cubes.values()) { + if (cubeRequest.effort.contains(Requirement.GENERATE)) { + Cube cube = cubeLoader.getLoadedCube(cubeRequest.getX(), cubeRequest.getY(), cubeRequest.getZ()); + + if (cube == null || !cube.isInitializedToLevel(CubeInitLevel.Generated)) { + toGenerate.add(cubeRequest.getY()); + } + } + } + + Chunk column = cubeLoader.getColumn(coord.chunkXPos, coord.chunkZPos, Requirement.GET_CACHED); + + if (column != null && toGenerate.isEmpty()) continue; + + ComputePlan plan = generator.plan(column, coord.chunkXPos, coord.chunkZPos, toGenerate); + + gpuEstimatedNs += scheduler.estimatePlanCost(plan); + plans.add(plan); + } + + if (!plans.isEmpty()) { + scheduler.submit(plans); + } + + profiler.endSection(); + } + @Override public String makeString() { return String.format("CubeProviderServer{loader=%s}", this.cubeLoader); @@ -506,6 +590,7 @@ public EagerCubeLoadRequest(CubePos pos, Requirement effort) { public void cancel() { this.cancelled = true; + CubeStatusVisualizer.remove(pos, CubeStatus.Enqueued); } @Override @@ -560,6 +645,8 @@ public EagerCubeLoadRequest loadCubeEagerly(int x, int y, int z, Requirement eff cubeLoader.preloadCube(pos, CubeInitLevel.fromRequirement(effort)); + CubeStatusVisualizer.put(pos, CubeStatus.Enqueued); + return request; } diff --git a/src/main/java/com/cardinalstar/cubicchunks/server/CubicPlayerManager.java b/src/main/java/com/cardinalstar/cubicchunks/server/CubicPlayerManager.java index 39a97e5b..67f60ced 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/server/CubicPlayerManager.java +++ b/src/main/java/com/cardinalstar/cubicchunks/server/CubicPlayerManager.java @@ -209,12 +209,14 @@ public void onCubeGenerated(Cube cube, CubeInitLevel newLevel) { } } - CubeStatusVisualizer.put(cube.getCoords(), switch (newLevel) { - case None -> CubeStatus.None; - case Generated -> CubeStatus.Generated; - case Populated -> CubeStatus.Populated; - case Lit -> CubeStatus.Lit; - }); + if (!cube.isEmpty()) { + CubeStatusVisualizer.put(cube.getCoords(), switch (newLevel) { + case None -> CubeStatus.None; + case Generated -> CubeStatus.Generated; + case Populated -> CubeStatus.Populated; + case Lit -> CubeStatus.Lit; + }); + } } @Override @@ -243,10 +245,19 @@ public void markBlockForUpdate(int x, int y, int z) { for (var player : players.getPlayerArray()) { player.sync.onBlockMarkedDirty(x, y, z); } + + CubeStatusVisualizer.put(cube.getCoords(), CubeStatus.Dirty); } } // Note these arguments are in global block coordinates + + public void onSurfaceTracked(Cube cube) { + for (var player : players.getPlayerArray()) { + player.sync.onSurfaceTracked(cube); + } + } + public void heightUpdated(int x, int z) { Chunk column = provider.getLoadedColumn(x >> 4, z >> 4); diff --git a/src/main/java/com/cardinalstar/cubicchunks/server/WorldSyncStateMachine.java b/src/main/java/com/cardinalstar/cubicchunks/server/WorldSyncStateMachine.java index 0f3d73ff..2bff1a8b 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/server/WorldSyncStateMachine.java +++ b/src/main/java/com/cardinalstar/cubicchunks/server/WorldSyncStateMachine.java @@ -167,6 +167,11 @@ public void onColumnHeightMarkedDirty(int x, int z) { .set(x & 0xF, z & 0xF); } + public void onSurfaceTracked(Cube cube) { + dirtyHeightCols.computeIfAbsent(cube.getX(), cube.getZ(), (x1, z1) -> new BooleanArray2D(16, 16)) + .set(0, 256, true); + } + public void onBlockMarkedDirty(int x, int y, int z) { dirtyBlocks.computeIfAbsent(x >> 4, y >> 4, z >> 4, (x1, y1, z1) -> new ShortOpenHashSet()) .add((short) AddressTools.getLocalAddress(x, y, z)); diff --git a/src/main/java/com/cardinalstar/cubicchunks/server/chunkio/CCNBTUtils.java b/src/main/java/com/cardinalstar/cubicchunks/server/chunkio/CCNBTUtils.java index 002f7273..b4bf5911 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/server/chunkio/CCNBTUtils.java +++ b/src/main/java/com/cardinalstar/cubicchunks/server/chunkio/CCNBTUtils.java @@ -1,5 +1,6 @@ package com.cardinalstar.cubicchunks.server.chunkio; +import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; @@ -9,42 +10,111 @@ import java.util.zip.GZIPOutputStream; import net.minecraft.nbt.CompressedStreamTools; +import net.minecraft.nbt.NBTBase; import net.minecraft.nbt.NBTSizeTracker; +import net.minecraft.nbt.NBTTagByteArray; import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagIntArray; +import net.minecraft.nbt.NBTTagString; +import net.minecraftforge.common.util.Constants.NBT; -import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.mutable.MutableInt; + +import com.cardinalstar.cubicchunks.mixin.early.common.AccessorNBTTagCompound; +import com.cardinalstar.cubicchunks.mixin.early.common.AccessorNBTTagList; public class CCNBTUtils { public static NBTTagCompound loadTag(byte[] data) throws IOException { - if (data[0] == (byte) 0x1f && data[1] == (byte) 0x8b) { - try (GZIPInputStream gzip = new GZIPInputStream(new ByteArrayInputStream(data));) { - data = IOUtils.toByteArray(gzip); - } + try (var input = new GZIPInputStream(new ByteArrayInputStream(data))) { + return CompressedStreamTools.func_152456_a(new DataInputStream(input), NBTSizeTracker.field_152451_a); } + } - return CompressedStreamTools - .func_152456_a(new DataInputStream(new ByteArrayInputStream(data)), NBTSizeTracker.field_152451_a); + public static byte[] saveTag(NBTTagCompound tag) throws IOException { + try (ByteArrayOutputStream nos = new ByteArrayOutputStream(getTagSizeEstimate(tag))) { + try (DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(new GZIPOutputStream2(nos)))) { + CompressedStreamTools.write(tag, dos); + } + + return nos.toByteArray(); + } } - public static byte[] saveTag(NBTTagCompound tag, boolean compress) throws IOException { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); + private static int getTagSizeEstimate(NBTBase tag) { + switch (tag.getId()) { + case NBT.TAG_BYTE -> { + return 2; + } + case NBT.TAG_SHORT -> { + return 3; + } + case NBT.TAG_INT -> { + return 5; + } + case NBT.TAG_LONG -> { + return 9; + } + case NBT.TAG_FLOAT -> { + return 5; + } + case NBT.TAG_DOUBLE -> { + return 9; + } + case NBT.TAG_BYTE_ARRAY -> { + return 5 + ((NBTTagByteArray) tag).func_150292_c().length; + } + case NBT.TAG_INT_ARRAY -> { + return 5 + ((NBTTagIntArray) tag).func_150302_c().length * 4; + } + case NBT.TAG_STRING -> { + return 1 + ((NBTTagString) tag).func_150285_a_() + .length() * 2; + } + case NBT.TAG_LIST -> { + var list = ((AccessorNBTTagList) tag).getTagList(); + + int len = list.size(); - CompressedStreamTools.write(tag, new DataOutputStream(baos)); + int size = 5; + + for (int i = 0; i < len; i++) { + size += getTagSizeEstimate(list.get(i)); + } + + return size; + } + case NBT.TAG_COMPOUND -> { + var map = ((AccessorNBTTagCompound) tag).getTagMap(); - byte[] data = baos.toByteArray(); + MutableInt size = new MutableInt(5); - if (compress) { - ByteArrayOutputStream zipped = new ByteArrayOutputStream(data.length); + map.forEach((key, value) -> { + size.add(key.length() * 2); + size.add(getTagSizeEstimate(value)); + }); - try (GZIPOutputStream zipstream = new GZIPOutputStream(zipped)) { - IOUtils.write(data, zipstream); - zipstream.flush(); + return size.intValue(); } + default -> { + return 1; + } + } + } - data = zipped.toByteArray(); + private static class GZIPOutputStream2 extends GZIPOutputStream { + + private final byte[] pooled; + + public GZIPOutputStream2(ByteArrayOutputStream nos) throws IOException { + super(nos); + pooled = new byte[1]; } - return data; + @Override + public void write(int b) throws IOException { + pooled[0] = (byte) (b & 0xff); + write(pooled, 0, 1); + } } } diff --git a/src/main/java/com/cardinalstar/cubicchunks/server/chunkio/CubeLoaderServer.java b/src/main/java/com/cardinalstar/cubicchunks/server/chunkio/CubeLoaderServer.java index 92cd5ef9..ee77148a 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/server/chunkio/CubeLoaderServer.java +++ b/src/main/java/com/cardinalstar/cubicchunks/server/chunkio/CubeLoaderServer.java @@ -457,14 +457,72 @@ private void invokeGenerateCallback(Cube cube, CubeInitLevel level) { } } + @Override + public void addColumn(Chunk column) { + ColumnInfo info = columns.get(column.xPosition, column.zPosition); + + if (info != null && info.column != null) { + CubicChunks.LOGGER.warn( + "addColumn tried to replace column at {},{}! (in-memory source: {}, attempted replacement source: {})", + column.xPosition, + column.zPosition, + info.source, + ObjectSource.Added); + return; + } + + if (info == null) { + info = new ColumnInfo(column.xPosition, column.zPosition); + columns.put(info); + } + + info.source = ObjectSource.Added; + info.column = column; + + info.onColumnLoaded(); + } + + @Override + public void addCube(Cube cube) { + CubeInfo info = cubes.get(cube.getX(), cube.getY(), cube.getZ()); + + if (info != null && info.cube != null) { + CubicChunks.LOGGER.warn( + "addCube tried to replace cube at {},{},{}! (in-memory source: {}, attempted replacement source: {})", + cube.getX(), + cube.getY(), + cube.getZ(), + info.source, + ObjectSource.Added); + return; + } + + if (info == null) { + info = new CubeInfo(cube.getX(), cube.getY(), cube.getZ()); + cubes.put(info); + } + + info.source = ObjectSource.Added; + info.cube = cube; + cube.setMeta(CUBE_INFO, info); + + info.ensureColumn(Requirement.GET_CACHED); + + info.onCubeLoaded(); + } + private void handleSideEffects(GenerationResult result, boolean doColumns, boolean doCubes) { if (doColumns) { for (Chunk column : result.columnSideEffects) { ColumnInfo info = columns.get(column.xPosition, column.zPosition); if (info != null && info.column != null) { - CubicChunks.LOGGER - .warn("Worldgen side-effect replaced column at {},{}!", column.xPosition, column.zPosition); + CubicChunks.LOGGER.warn( + "Worldgen side-effect tried to replace column at {},{}! (in-memory source: {}, attempted replacement source: {})", + column.xPosition, + column.zPosition, + info.source, + ObjectSource.GeneratedSideEffect); continue; } @@ -485,8 +543,13 @@ private void handleSideEffects(GenerationResult result, boolean doColumns, bo CubeInfo info = cubes.get(cube.getX(), cube.getY(), cube.getZ()); if (info != null && info.cube != null) { - CubicChunks.LOGGER - .warn("Worldgen side-effect replaced cube at {},{},{}!", cube.getX(), cube.getY(), cube.getZ()); + CubicChunks.LOGGER.warn( + "Worldgen side-effect tried to replace cube at {},{},{}! (in-memory source: {}, attempted replacement source: {})", + cube.getX(), + cube.getY(), + cube.getZ(), + info.source, + ObjectSource.GeneratedSideEffect); continue; } @@ -510,6 +573,7 @@ private enum ObjectSource { None, Disk, Generated, + Added, GeneratedSideEffect, Boundary } @@ -713,10 +777,11 @@ private void loadBoundaryCube() { if (this.column == null) { CubicChunks.LOGGER.error( - "Tried to load a cube that did not have a saved column: it will be regenerated ({},{},{})", + "Tried to load a cube that did not have a saved column: it will be regenerated ({},{},{}, source={})", getX(), getY(), getZ(), + this.source, new Exception()); this.cube = null; this.tag = null; @@ -724,6 +789,8 @@ private void loadBoundaryCube() { } this.cube = new BoundaryCube(this.column.column, this.getY()); + this.source = ObjectSource.Boundary; + onCubeLoaded(); } @@ -746,10 +813,11 @@ private void loadCube() throws IOException { if (this.column == null) { CubicChunks.LOGGER.error( - "Tried to load a cube that did not have a saved column: it will be regenerated ({},{},{})", + "Tried to load a cube that did not have a saved column: it will be regenerated ({},{},{}, source={})", getX(), getY(), getZ(), + this.source, new Exception()); this.cube = null; this.tag = null; @@ -833,8 +901,11 @@ private boolean generate(CubeInitLevel requestedInitLevel) { if (requestedInitLevel == CubeInitLevel.Generated) return generated; if (!generated) return false; - // If this cube hasn't been populated at all, populate it. This generates any required cubes recursively. - generator.populate(cube); + if (!isInitedTo(CubeInitLevel.Populated)) { + // If this cube hasn't been populated at all, populate it. This generates any required cubes + // recursively. + generator.populate(cube); + } boolean populated = isInitedTo(CubeInitLevel.Populated); diff --git a/src/main/java/com/cardinalstar/cubicchunks/server/chunkio/ICubeLoader.java b/src/main/java/com/cardinalstar/cubicchunks/server/chunkio/ICubeLoader.java index 13731b8e..74820cb2 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/server/chunkio/ICubeLoader.java +++ b/src/main/java/com/cardinalstar/cubicchunks/server/chunkio/ICubeLoader.java @@ -55,4 +55,8 @@ default void cacheCubes(Box box, Requirement effort) { void saveCube(Cube cube); void doGC(); + + void addColumn(Chunk column); + + void addCube(Cube cube); } diff --git a/src/main/java/com/cardinalstar/cubicchunks/server/chunkio/RegionCubeStorage.java b/src/main/java/com/cardinalstar/cubicchunks/server/chunkio/RegionCubeStorage.java index 4d3da278..0b76639e 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/server/chunkio/RegionCubeStorage.java +++ b/src/main/java/com/cardinalstar/cubicchunks/server/chunkio/RegionCubeStorage.java @@ -284,7 +284,7 @@ private Map compressNBTForBatchWrite(Map keyMappingFunction.apply(entry.getKey()), entry -> { try { - return CCNBTUtils.saveTag(entry.getValue(), true); + return CCNBTUtils.saveTag(entry.getValue()); } catch (IOException e) { // wrap exception so that we can throw it from inside the lambda throw new UncheckedIOException(e); diff --git a/src/main/java/com/cardinalstar/cubicchunks/util/CubeStatusVisualizer.java b/src/main/java/com/cardinalstar/cubicchunks/util/CubeStatusVisualizer.java index 640bc94c..1856616f 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/util/CubeStatusVisualizer.java +++ b/src/main/java/com/cardinalstar/cubicchunks/util/CubeStatusVisualizer.java @@ -1,12 +1,19 @@ package com.cardinalstar.cubicchunks.util; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.server.MinecraftServer; +import net.minecraft.util.AxisAlignedBB; import com.cardinalstar.cubicchunks.CubicChunksConfig; +import com.cardinalstar.cubicchunks.util.boxvisualizer.BoxVisualizer; +import com.cardinalstar.cubicchunks.util.boxvisualizer.VisualizedBox; +import com.gtnewhorizon.gtnhlib.color.RGBColor; import com.gtnewhorizon.gtnhlib.eventbus.EventBusSubscriber; import cpw.mods.fml.common.eventhandler.SubscribeEvent; @@ -17,12 +24,20 @@ public class CubeStatusVisualizer { public enum CubeStatus { - None, - Generated, - Populated, - Lit, - Dirty, - Synced + + Enqueued(RGBColor.fromRGBA(0x64292832)), + None(RGBColor.fromRGBA(0x64326432)), + Generated(RGBColor.fromRGBA(0x32C83232)), + Populated(RGBColor.fromRGBA(0x3232C832)), + Lit(RGBColor.fromRGBA(0xC8C83232)), + Dirty(RGBColor.fromRGBA(0x0EE5BB32)), + Synced(null); + + public final RGBColor color; + + CubeStatus(RGBColor color) { + this.color = color; + } } private static final ConcurrentHashMap cubeStatus = new ConcurrentHashMap<>(); @@ -45,57 +60,54 @@ public static void sync(ServerTickEvent event) { wasSent = false; for (EntityPlayerMP player : MinecraftServer.getServer() .getConfigurationManager().playerEntityList) { - // BoxVisualizer.sendBoxes(player, Duration.ofMinutes(0), new ArrayList<>(), true); + BoxVisualizer.sendBoxes(player, Duration.ofMinutes(0), new ArrayList<>(), true); } } return; } - // List boxes = new ArrayList<>(); + List boxes = new ArrayList<>(); cubeStatus.forEach((pos, status) -> { - // boxes.add(new VisualizedBox( - // switch (status) { - // case None -> new Color(100, 50, 100, 50); - // case Generated -> new Color(50, 200, 50, 50); - // case Populated -> new Color(50, 50, 200, 50); - // case Lit -> new Color(200, 200, 50, 50); - // case Dirty -> new Color(14, 229, 187, 50); - // case Synced -> new Color(200, 50, 50, 50); - // }, - // new AABBd( - // pos.getMinBlockX() - 0.5, pos.getMinBlockY() + 0.5, pos.getMinBlockZ() - 0.5, - // pos.getMaxBlockX() - 0.5, pos.getMaxBlockY() - 0.5, pos.getMaxBlockZ() - 0.5) - // )); + if (status == CubeStatus.Synced) return; + + AxisAlignedBB boundingBox = AxisAlignedBB.getBoundingBox( + pos.getMinBlockX() - 0.5, + pos.getMinBlockY() + 0.5, + pos.getMinBlockZ() - 0.5, + pos.getMaxBlockX() - 0.5, + pos.getMaxBlockY() - 0.5, + pos.getMaxBlockZ() - 0.5); + + boxes.add(new VisualizedBox(status.color, boundingBox)); }); wasSent = true; for (EntityPlayerMP player : MinecraftServer.getServer() .getConfigurationManager().playerEntityList) { - // BoxVisualizer.sendBoxes(player, Duration.ofMinutes(5), boxes, true); + BoxVisualizer.sendBoxes(player, Duration.ofMinutes(5), boxes, false); } } public static void put(CubePos pos, CubeStatus status) { - if (pos.getY() != 4) return; - cubeStatus.put(pos, status); dirty.set(true); } public static void cmpexc(CubePos pos, CubeStatus expected, CubeStatus desired) { - if (pos.getY() != 4) return; - cubeStatus.compute(pos, (key, existing) -> existing == expected ? desired : existing); dirty.set(true); } public static void remove(CubePos pos) { - if (pos.getY() != 4) return; - cubeStatus.remove(pos); dirty.set(true); } + + public static void remove(CubePos pos, CubeStatus expected) { + cubeStatus.remove(pos, expected); + dirty.set(true); + } } diff --git a/src/main/java/com/cardinalstar/cubicchunks/util/DataUtils.java b/src/main/java/com/cardinalstar/cubicchunks/util/DataUtils.java index 20322d64..a3378e1f 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/util/DataUtils.java +++ b/src/main/java/com/cardinalstar/cubicchunks/util/DataUtils.java @@ -124,6 +124,14 @@ public static T find(Collection list, Predicate filter) { return null; } + public static boolean contains(T[] array, T object) { + for (T val : array) { + if (Objects.equals(val, object)) return true; + } + + return false; + } + @SuppressWarnings("unchecked") public static R findInstance(Collection list, Class clazz) { for (T value : list) { diff --git a/src/main/java/com/cardinalstar/cubicchunks/util/IntFraction.java b/src/main/java/com/cardinalstar/cubicchunks/util/IntFraction.java new file mode 100644 index 00000000..c4de9ade --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/util/IntFraction.java @@ -0,0 +1,67 @@ +package com.cardinalstar.cubicchunks.util; + +import gregtech.api.util.GTUtility; +import gtPlusPlus.core.util.math.MathUtils; + +public class IntFraction { + + public long numerator = 1, denominator = 1; + + public IntFraction() { + + } + + public IntFraction(long numerator, long denominator) { + this.numerator = numerator; + this.denominator = denominator; + } + + public double toDouble() { + return numerator / (double) denominator; + } + + public float toFloat() { + return numerator / (float) denominator; + } + + public IntFraction clone() { + return new IntFraction(numerator, denominator); + } + + public IntFraction mul(long n) { + this.numerator *= n; + return this; + } + + public IntFraction div(long n) { + this.denominator *= n; + return this; + } + + public IntFraction reduce() { + long gcd = MathUtils.gcd(this.numerator, this.denominator); + + if (gcd > 1) { + this.numerator /= gcd; + this.denominator /= gcd; + } + + return this; + } + + public int apply(int x) { + return (int) apply((long) x); + } + + public int applyCeil(int x) { + return (int) applyCeil((long) x); + } + + public long apply(long x) { + return x * numerator / denominator; + } + + public long applyCeil(long x) { + return GTUtility.ceilDiv(x * numerator, denominator); + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/util/JavaUtils.java b/src/main/java/com/cardinalstar/cubicchunks/util/JavaUtils.java new file mode 100644 index 00000000..2181fd5e --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/util/JavaUtils.java @@ -0,0 +1,26 @@ +package com.cardinalstar.cubicchunks.util; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodType; + +import lombok.SneakyThrows; + +public class JavaUtils { + + public static final int JVM_VERSION = Integer.parseInt(System.getProperty("java.specification.version")); + + @SneakyThrows + public static void onSpinWait() { + if (JVM_VERSION >= 9) { + Java9.ON_SPIN_WAIT.invokeExact(); + } + } + + /// Inner class to avoid class loading + private static class Java9 { + + public static final MethodHandle ON_SPIN_WAIT = DataUtils + .exposeMethod(Thread.class, MethodType.methodType(void.class), "onSpinWait"); + } + +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/util/MathUtil.java b/src/main/java/com/cardinalstar/cubicchunks/util/MathUtil.java index 8368192b..52019052 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/util/MathUtil.java +++ b/src/main/java/com/cardinalstar/cubicchunks/util/MathUtil.java @@ -35,6 +35,10 @@ public static int ceilDiv(int a, int b) { return -Math.floorDiv(-a, b); } + public static long ceilDiv(long a, long b) { + return -Math.floorDiv(-a, b); + } + public static boolean isPowerOfN(int toTest, int n) { // works only for positive numbers while (toTest > n - 1 && toTest % n == 0) { toTest /= n; @@ -205,4 +209,36 @@ public static double distanceSq(double toX, double toY, double toZ, ChunkCoordin double d2 = coords.posZ - toZ; return d0 * d0 + d1 * d1 + d2 * d2; } + + public static int alignTo(int value, int alignment) { + if (Integer.bitCount(alignment) == 1) { + int log2L = Long.numberOfTrailingZeros(alignment); + + int mask = (1 << log2L) - 1; + + int result = value & ~mask; + + if ((value & mask) != 0) result += (1 << log2L); + + return result; + } else { + return ceilDiv(value, alignment) * alignment; + } + } + + public static long alignTo(long value, long alignment) { + if (Long.bitCount(alignment) == 1) { + int log2L = Long.numberOfTrailingZeros(alignment); + + long mask = (1L << log2L) - 1; + + long result = value & ~mask; + + if ((value & mask) != 0) result += (1L << log2L); + + return result; + } else { + return ceilDiv(value, alignment) * alignment; + } + } } diff --git a/src/main/java/com/cardinalstar/cubicchunks/util/boxvisualizer/BoxVisualizer.java b/src/main/java/com/cardinalstar/cubicchunks/util/boxvisualizer/BoxVisualizer.java new file mode 100644 index 00000000..6826935b --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/util/boxvisualizer/BoxVisualizer.java @@ -0,0 +1,33 @@ +package com.cardinalstar.cubicchunks.util.boxvisualizer; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import net.minecraft.entity.player.EntityPlayerMP; + +import com.cardinalstar.cubicchunks.network.PacketEncoderUpdateVisualizedBoxes.PacketUpdateVisualizedBoxes; + +public class BoxVisualizer { + + private static final int MAX_PACKET_SIZE = 3200, BYTES_PER_BOX = 4 + 4 * 3 + 4 * 3, + MAX_BOXES_PER_PACKET = MAX_PACKET_SIZE / BYTES_PER_BOX; + + public static void sendBoxes(EntityPlayerMP player, Duration timeout, Collection boxes, + boolean disableDepth) { + List boxList = new ArrayList<>(boxes); + + for (int i = 0; i < boxList.size(); i += MAX_BOXES_PER_PACKET) { + int toSend = Math.min(boxList.size() - i, MAX_BOXES_PER_PACKET); + + PacketUpdateVisualizedBoxes packet = new PacketUpdateVisualizedBoxes( + timeout.toMillis(), + i > 0, + disableDepth, + boxList.subList(i, i + toSend)); + + packet.sendToPlayer(player); + } + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/util/boxvisualizer/QuadSorter.java b/src/main/java/com/cardinalstar/cubicchunks/util/boxvisualizer/QuadSorter.java new file mode 100644 index 00000000..0cbf59fe --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/util/boxvisualizer/QuadSorter.java @@ -0,0 +1,124 @@ +package com.cardinalstar.cubicchunks.util.boxvisualizer; + +import java.nio.FloatBuffer; +import java.util.BitSet; + +import org.joml.Math; + +import com.google.common.primitives.Floats; +import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat; + +import it.unimi.dsi.fastutil.ints.IntArrays; + +public class QuadSorter { + + public static void sortStandardFormat(VertexFormat format, FloatBuffer buffer, int bufferLen, float x, float y, + float z) { + // Quad stride by Float size + int quadStride = format.getVertexSize(); + + int quadCount = bufferLen / quadStride / 4; + + float[] distanceArray = new float[quadCount]; + int[] indicesArray = new int[quadCount]; + + int vertexSizeInteger = quadStride / 4; + + for (int quadIdx = 0; quadIdx < quadCount; ++quadIdx) { + distanceArray[quadIdx] = getDistanceSqSFP(buffer, x, y, z, vertexSizeInteger, quadIdx * quadStride); + indicesArray[quadIdx] = quadIdx; + } + + IntArrays.mergeSort(indicesArray, (a, b) -> Floats.compare(distanceArray[b], distanceArray[a])); + + rearrangeQuads(buffer, indicesArray, quadStride); + } + + public static void rearrangeQuads(FloatBuffer buffer, int[] indicesArray, int stride) { + BitSet bits = new BitSet(); + + float[] temp1 = new float[stride]; + float[] temp2 = new float[stride]; + float[] temp3 = new float[stride]; + + for (int l = bits.nextClearBit(0); l < indicesArray.length; l = bits.nextClearBit(l + 1)) { + int m = indicesArray[l]; + + if (m != l) { + buffer.position(m * stride); + buffer.get(temp1); + + int n = m; + + for (int o = indicesArray[m]; n != l; o = indicesArray[o]) { + buffer.position(n * stride); + buffer.get(temp2); + buffer.position(o * stride); + buffer.get(temp3); + + buffer.position(n * stride); + buffer.put(temp3); + buffer.position(o * stride); + buffer.put(temp2); + + bits.set(n); + n = o; + } + + buffer.position(l * stride); + buffer.put(temp1); + } + + bits.set(l); + } + } + + private static float getDistanceSqSFP(FloatBuffer buffer, float xCenter, float yCenter, float zCenter, int stride, + int start) { + int vertexBase = start; + final float x1 = buffer.get(vertexBase); + final float y1 = buffer.get(vertexBase + 1); + final float z1 = buffer.get(vertexBase + 2); + + vertexBase += stride; + final float x2 = buffer.get(vertexBase); + final float y2 = buffer.get(vertexBase + 1); + final float z2 = buffer.get(vertexBase + 2); + + vertexBase += stride; + final float x3 = buffer.get(vertexBase); + final float y3 = buffer.get(vertexBase + 1); + final float z3 = buffer.get(vertexBase + 2); + + // vertexBase += stride; + // final float x4 = buffer.get(vertexBase); + // final float y4 = buffer.get(vertexBase + 1); + // final float z4 = buffer.get(vertexBase + 2); + + final float xa = x2 - x1; + final float ya = y2 - y1; + final float za = z2 - z1; + + final float xb = x3 - x1; + final float yb = y3 - y1; + final float zb = z3 - z1; + + float nx = org.joml.Math.fma(ya, zb, -za * yb); + float ny = org.joml.Math.fma(za, xb, -xa * zb); + float nz = Math.fma(xa, yb, -ya * xb); + + float mag = 1f / Math.sqrt(nx * nx + ny * ny + nz * nz); + + nx *= mag; + ny *= mag; + nz *= mag; + + return nx * xCenter + ny * yCenter + nz * zCenter; + + // final float xDist = ((x1 + x2 + x3 + x4) * 0.25F) - xCenter; + // final float yDist = ((y1 + y2 + y3 + y4) * 0.25F) - yCenter; + // final float zDist = ((z1 + z2 + z3 + z4) * 0.25F) - zCenter; + // + // return (xDist * xDist) + (yDist * yDist) + (zDist * zDist); + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/util/boxvisualizer/VisualizedBox.java b/src/main/java/com/cardinalstar/cubicchunks/util/boxvisualizer/VisualizedBox.java new file mode 100644 index 00000000..f8a950e3 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/util/boxvisualizer/VisualizedBox.java @@ -0,0 +1,38 @@ +package com.cardinalstar.cubicchunks.util.boxvisualizer; + +import net.minecraft.util.AxisAlignedBB; + +import com.gtnewhorizon.gtnhlib.color.RGBColor; + +public class VisualizedBox { + + public final RGBColor color; + public AxisAlignedBB bounds; + + public VisualizedBox(RGBColor color, AxisAlignedBB bounds) { + this.color = color; + this.bounds = bounds; + } + + public VisualizedBox(int rgba, AxisAlignedBB bounds) { + this.color = RGBColor.fromRGBA(rgba); + this.bounds = bounds; + } + + public VisualizedBox expand(double amount) { + bounds = AxisAlignedBB.getBoundingBox( + bounds.minX - amount, + bounds.minY - amount, + bounds.minZ - amount, + bounds.maxX + amount, + bounds.maxY + amount, + bounds.maxZ + amount); + + return this; + } + + @Override + public String toString() { + return "VisualizedBox{" + "color=" + color + ", bounds=" + bounds + '}'; + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/util/boxvisualizer/VisualizedBoxRenderer.java b/src/main/java/com/cardinalstar/cubicchunks/util/boxvisualizer/VisualizedBoxRenderer.java new file mode 100644 index 00000000..1d2fee49 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/util/boxvisualizer/VisualizedBoxRenderer.java @@ -0,0 +1,174 @@ +package com.cardinalstar.cubicchunks.util.boxvisualizer; + +import java.nio.ByteBuffer; +import java.util.List; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.entity.Entity; +import net.minecraft.util.AxisAlignedBB; +import net.minecraftforge.client.event.RenderWorldLastEvent; + +import org.lwjgl.opengl.GL11; + +import com.gtnewhorizon.gtnhlib.client.renderer.TessellatorManager; +import com.gtnewhorizon.gtnhlib.client.renderer.vbo.VertexBuffer; +import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat; +import com.gtnewhorizon.gtnhlib.eventbus.EventBusSubscriber; + +import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; + +@EventBusSubscriber(side = Side.CLIENT) +@SideOnly(Side.CLIENT) +public class VisualizedBoxRenderer { + + private static final VertexBuffer VBO = new VertexBuffer(DefaultVertexFormat.POSITION_COLOR_TEXTURE, GL11.GL_QUADS); + + private static long timeout; + private static boolean disableDepth; + private static List boxes; + + public static void receiveBoxes(long timeout, boolean append, List boxes, boolean disableDepth) { + VisualizedBoxRenderer.timeout = System.currentTimeMillis() + timeout; + VisualizedBoxRenderer.disableDepth = disableDepth; + + if (append) { + VisualizedBoxRenderer.boxes.addAll(boxes); + } else { + VisualizedBoxRenderer.boxes = boxes; + } + } + + @SubscribeEvent + public static void renderBoxes(RenderWorldLastEvent event) { + if (boxes == null || boxes.isEmpty()) return; + if (timeout < System.currentTimeMillis()) { + boxes = null; + return; + } + + Entity player = Minecraft.getMinecraft().renderViewEntity; + double xd = player.lastTickPosX + (player.posX - player.lastTickPosX) * event.partialTicks; + double yd = player.lastTickPosY + (player.posY - player.lastTickPosY) * event.partialTicks; + double zd = player.lastTickPosZ + (player.posZ - player.lastTickPosZ) * event.partialTicks; + + TessellatorManager.startCapturing(); + + Tessellator tessellator = TessellatorManager.get(); + + tessellator.startDrawingQuads(); + + for (VisualizedBox box : boxes) { + tessellator + .setColorRGBA(box.color.getRed(), box.color.getGreen(), box.color.getBlue(), box.color.getAlpha()); + + drawBox(tessellator, xd, yd, zd, box.bounds); + } + + tessellator.draw(); + + ByteBuffer quads = TessellatorManager.stopCapturingToBuffer(DefaultVertexFormat.POSITION_COLOR_TEXTURE); + + QuadSorter.sortStandardFormat( + DefaultVertexFormat.POSITION_COLOR_TEXTURE, + quads.asFloatBuffer(), + quads.capacity(), + (float) xd, + (float) yd, + (float) zd); + + GL11.glPushAttrib(GL11.GL_ENABLE_BIT | GL11.GL_COLOR_BUFFER_BIT); + + if (disableDepth) GL11.glDisable(GL11.GL_DEPTH_TEST); + GL11.glDisable(GL11.GL_TEXTURE_2D); + + GL11.glDisable(GL11.GL_CULL_FACE); + GL11.glDisable(GL11.GL_ALPHA_TEST); + GL11.glEnable(GL11.GL_BLEND); // enable blend so it is transparent + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + + VBO.uploadStream(quads); + VBO.render(); + + GL11.glPopAttrib(); + } + + private static void drawBox(Tessellator tes, double eyeX, double eyeY, double eyeZ, AxisAlignedBB bounds) { + double X = bounds.minX - eyeX; + double Y = bounds.minY - eyeY; + double Z = bounds.minZ - eyeZ; + double worldX = bounds.minX; + double worldY = bounds.minY; + double worldZ = bounds.minZ; + + double sX = bounds.maxX - bounds.minX; + double sY = bounds.maxY - bounds.minY; + double sZ = bounds.maxZ - bounds.minZ; + + // this rendering code is independently written by glee8e on July 10th, 2023 + // and is released as part of StructureLib under LGPL terms, just like everything else in this project + // cube is a very special model. its facings can be rendered correctly by viewer distance without using + // surface normals and view vector + // here we do a 2 pass render. + // first pass we draw obstructed faces (i.e. faces that are further away from player) + // second pass we draw unobstructed faces + for (int j = 0; j < 2; j++) { + boolean unobstructedPass = j == 1; + for (int i = 0; i < 6; i++) { + switch (i) { // {DOWN, UP, NORTH, SOUTH, WEST, EAST} + case 0 -> { + // all these ifs is in form if ((is face unobstructed) != (is in unobstructred pass)) + if (worldY >= eyeY != unobstructedPass) continue; + tes.setNormal(0, -1, 0); + tes.addVertex(X, Y, Z); + tes.addVertex(X + sX, Y, Z); + tes.addVertex(X + sX, Y, Z + sZ); + tes.addVertex(X, Y, Z + sZ); + } + case 1 -> { + if (worldY + sY <= eyeY != unobstructedPass) continue; + tes.setNormal(0, 1, 0); + tes.addVertex(X, Y + sY, Z); + tes.addVertex(X, Y + sY, Z + sZ); + tes.addVertex(X + sX, Y + sY, Z + sZ); + tes.addVertex(X + sX, Y + sY, Z); + } + case 2 -> { + if (worldZ >= eyeZ != unobstructedPass) continue; + tes.setNormal(0, 0, -1); + tes.addVertex(X, Y, Z); + tes.addVertex(X, Y + sY, Z); + tes.addVertex(X + sX, Y + sY, Z); + tes.addVertex(X + sX, Y, Z); + } + case 3 -> { + if (worldZ + sZ <= eyeZ != unobstructedPass) continue; + tes.setNormal(0, 0, 1); + tes.addVertex(X + sX, Y, Z + sZ); + tes.addVertex(X + sX, Y + sY, Z + sZ); + tes.addVertex(X, Y + sY, Z + sZ); + tes.addVertex(X, Y, Z + sZ); + } + case 4 -> { + if (worldX >= eyeX != unobstructedPass) continue; + tes.setNormal(-1, 0, 0); + tes.addVertex(X, Y, Z + sZ); + tes.addVertex(X, Y + sY, Z + sZ); + tes.addVertex(X, Y + sY, Z); + tes.addVertex(X, Y, Z); + } + case 5 -> { + if (worldX + sX <= eyeX != unobstructedPass) continue; + tes.setNormal(1, 0, 0); + tes.addVertex(X + sX, Y, Z); + tes.addVertex(X + sX, Y + sY, Z); + tes.addVertex(X + sX, Y + sY, Z + sZ); + tes.addVertex(X + sX, Y, Z + sZ); + } + } + } + } + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/world/cube/Cube.java b/src/main/java/com/cardinalstar/cubicchunks/world/cube/Cube.java index f0835cfd..7abfda43 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/world/cube/Cube.java +++ b/src/main/java/com/cardinalstar/cubicchunks/world/cube/Cube.java @@ -624,7 +624,6 @@ public void onCubeLoad() { EVENT_BUS.post(new CubeEvent.Load(world, this)); } - @SuppressWarnings("deprecation") public void trackSurface() { IHeightMap opindex = ((IColumn) column).getOpacityIndex(); int miny = getCoords().getMinBlockY(); diff --git a/src/main/java/com/cardinalstar/cubicchunks/world/layer/LayeredWorld.java b/src/main/java/com/cardinalstar/cubicchunks/world/layer/LayeredWorld.java new file mode 100644 index 00000000..b6f8fb9a --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/world/layer/LayeredWorld.java @@ -0,0 +1,141 @@ +package com.cardinalstar.cubicchunks.world.layer; + +import java.io.File; + +import net.minecraft.block.Block; +import net.minecraft.entity.Entity; +import net.minecraft.init.Blocks; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.profiler.Profiler; +import net.minecraft.world.World; +import net.minecraft.world.WorldProvider; +import net.minecraft.world.WorldSettings; +import net.minecraft.world.chunk.IChunkProvider; +import net.minecraft.world.chunk.storage.IChunkLoader; +import net.minecraft.world.storage.IPlayerFileData; +import net.minecraft.world.storage.ISaveHandler; +import net.minecraft.world.storage.WorldInfo; + +import com.cardinalstar.cubicchunks.util.XSTR; + +public class LayeredWorld extends World { + + private final World next; + + public LayeredWorld(World next) { + super( + new DummySaveHandler(), + "DUMMY_DIMENSION", + null, + new WorldSettings(new WorldInfo(new NBTTagCompound())), + new Profiler()); + this.next = next; + + this.rand = new XSTR(); + this.chunkProvider = next.getChunkProvider(); + } + + @Override + protected IChunkProvider createChunkProvider() { + return null; + } + + @Override + public Entity getEntityByID(int aEntityID) { + return next.getEntityByID(aEntityID); + } + + @Override + public boolean setBlock(int aX, int aY, int aZ, Block aBlock, int aMeta, int aFlags) { + return true; + } + + // @Override + // public float getSunBrightnessFactor(float p_72967_1_) { + // return base.getSunBrightnessFactor(p_72967_1_); + // } + + // @Override + // public BiomeGenBase getBiomeGenForCoords(int aX, int aZ) { + // return base.getBiomeGenForCoords(aX, aZ); + // } + // + // @Override + // public int getFullBlockLightValue(int aX, int aY, int aZ) { + // return base.getFullBlockLightValue(aX, aY, aZ); + // } + + @Override + public Block getBlock(int aX, int aY, int aZ) { + return Blocks.air; + } + + // @Override + // public int getBlockMetadata(int aX, int aY, int aZ) { + // if (aX == airX && aY == airY && aZ == airZ) return 0; + // + // return world.getBlockMetadata(aX, aY, aZ); + // } + // + // @Override + // public TileEntity getTileEntity(int aX, int aY, int aZ) { + // if (aX == airX && aY == airY && aZ == airZ) return null; + // + // return world.getTileEntity(aX, aY, aZ); + // } + // + // @Override + // public boolean canBlockSeeTheSky(int aX, int aY, int aZ) { + // return world.canBlockSeeTheSky(aX, aY, aZ); + // } + + @Override + protected int func_152379_p() { + return 0; + } + + private static class DummySaveHandler implements ISaveHandler { + + @Override + public void saveWorldInfoWithPlayer(WorldInfo worldInfo, NBTTagCompound nbtTagCompound) {} + + @Override + public void saveWorldInfo(WorldInfo worldInfo) {} + + @Override + public WorldInfo loadWorldInfo() { + return null; + } + + @Override + public IPlayerFileData getSaveHandler() { + return null; + } + + @Override + public File getMapFileFromName(String mapName) { + return null; + } + + @Override + public IChunkLoader getChunkLoader(WorldProvider worldProvider) { + return null; + } + + @Override + public void flush() {} + + @Override + public void checkSessionLock() {} + + @Override + public String getWorldDirectoryName() { + return null; + } + + @Override + public File getWorldDirectory() { + return null; + } + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/world/layer/ProxiedWorld.java b/src/main/java/com/cardinalstar/cubicchunks/world/layer/ProxiedWorld.java new file mode 100644 index 00000000..b64e7225 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/world/layer/ProxiedWorld.java @@ -0,0 +1,140 @@ +package com.cardinalstar.cubicchunks.world.layer; + +import java.io.File; + +import net.minecraft.block.Block; +import net.minecraft.entity.Entity; +import net.minecraft.init.Blocks; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.profiler.Profiler; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.world.World; +import net.minecraft.world.WorldProvider; +import net.minecraft.world.WorldSettings; +import net.minecraft.world.biome.BiomeGenBase; +import net.minecraft.world.chunk.IChunkProvider; +import net.minecraft.world.chunk.storage.IChunkLoader; +import net.minecraft.world.storage.IPlayerFileData; +import net.minecraft.world.storage.ISaveHandler; +import net.minecraft.world.storage.WorldInfo; + +import com.cardinalstar.cubicchunks.util.XSTR; + +public class ProxiedWorld extends World { + + private final World world; + + public int airX, airY, airZ; + + public ProxiedWorld(World world) { + super(new ISaveHandler() { + + @Override + public void saveWorldInfoWithPlayer(WorldInfo worldInfo, NBTTagCompound nbtTagCompound) {} + + @Override + public void saveWorldInfo(WorldInfo worldInfo) {} + + @Override + public WorldInfo loadWorldInfo() { + return null; + } + + @Override + public IPlayerFileData getSaveHandler() { + return null; + } + + @Override + public File getMapFileFromName(String mapName) { + return null; + } + + @Override + public IChunkLoader getChunkLoader(WorldProvider worldProvider) { + return null; + } + + @Override + public void flush() {} + + @Override + public void checkSessionLock() {} + + @Override + public String getWorldDirectoryName() { + return null; + } + + @Override + public File getWorldDirectory() { + return null; + } + }, "DUMMY_DIMENSION", null, new WorldSettings(new WorldInfo(new NBTTagCompound())), new Profiler()); + + this.rand = new XSTR(); + this.world = world; + this.chunkProvider = world.getChunkProvider(); + } + + @Override + protected IChunkProvider createChunkProvider() { + return null; + } + + @Override + public Entity getEntityByID(int aEntityID) { + return null; + } + + @Override + public boolean setBlock(int aX, int aY, int aZ, Block aBlock, int aMeta, int aFlags) { + return true; + } + + @Override + public float getSunBrightnessFactor(float p_72967_1_) { + return world.getSunBrightnessFactor(p_72967_1_); + } + + @Override + public BiomeGenBase getBiomeGenForCoords(int aX, int aZ) { + return world.getBiomeGenForCoords(aX, aZ); + } + + @Override + public int getFullBlockLightValue(int aX, int aY, int aZ) { + return world.getFullBlockLightValue(aX, aY, aZ); + } + + @Override + public Block getBlock(int aX, int aY, int aZ) { + if (aX == airX && aY == airY && aZ == airZ) return Blocks.air; + + return world.getBlock(aX, aY, aZ); + } + + @Override + public int getBlockMetadata(int aX, int aY, int aZ) { + if (aX == airX && aY == airY && aZ == airZ) return 0; + + return world.getBlockMetadata(aX, aY, aZ); + } + + @Override + public TileEntity getTileEntity(int aX, int aY, int aZ) { + if (aX == airX && aY == airY && aZ == airZ) return null; + + return world.getTileEntity(aX, aY, aZ); + } + + @Override + public boolean canBlockSeeTheSky(int aX, int aY, int aZ) { + return world.canBlockSeeTheSky(aX, aY, aZ); + } + + @Override + protected int func_152379_p() { + return 0; + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/world/layer/WorldLayer.java b/src/main/java/com/cardinalstar/cubicchunks/world/layer/WorldLayer.java new file mode 100644 index 00000000..66ab9d4b --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/world/layer/WorldLayer.java @@ -0,0 +1,57 @@ +package com.cardinalstar.cubicchunks.world.layer; + +import javax.annotation.Nullable; + +import net.minecraft.block.Block; +import net.minecraft.tileentity.TileEntity; + +import org.apache.commons.lang3.mutable.MutableInt; + +import com.cardinalstar.cubicchunks.util.BooleanArray2D; +import com.cardinalstar.cubicchunks.util.BooleanArray3D; +import com.cardinalstar.cubicchunks.util.HashMap2D; +import com.cardinalstar.cubicchunks.util.HashMap3D; + +import it.unimi.dsi.fastutil.ints.Int2ObjectRBTreeMap; + +public class WorldLayer { + + private final HashMap2D chunks = new HashMap2D<>(); + private final HashMap3D data = new HashMap3D<>(); + private final HashMap3D tiles = new HashMap3D<>(); + + @Nullable + public Block getBlock(int x, int y, int z) { + var ebs = data.get(x >> 4, y >> 4, z >> 4); + + if (ebs == null) return null; + if (!ebs.presence.get(x & 0xF, y & 0xF, z & 0xF)) return null; + + return ebs.blocks[((x & 0xF) << 8) | ((y & 0xF) << 4) | (z & 0xF)]; + } + + public boolean getBlockMeta(int x, int y, int z, MutableInt meta) { + var ebs = data.get(x >> 4, y >> 4, z >> 4); + + if (ebs == null) return false; + if (!ebs.presence.get(x & 0xF, y & 0xF, z & 0xF)) return false; + + meta.setValue(ebs.meta[((x & 0xF) << 8) | ((y & 0xF) << 4) | (z & 0xF)]); + return true; + } + + private static class PseudoChunk { + + public final int[] heightmap = new int[16 * 16]; + public final BooleanArray2D presencce = new BooleanArray2D(16, 16); + + public final Int2ObjectRBTreeMap ebs = new Int2ObjectRBTreeMap<>(); + } + + private static class PseudoEBS { + + public final Block[] blocks = new Block[16 * 16 * 16]; + public final int[] meta = new int[16 * 16 * 16]; + public final BooleanArray3D presence = new BooleanArray3D(16, 16, 16); + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/WorldGenerators.java b/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/WorldGenerators.java index 34bd07aa..a9d8f903 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/WorldGenerators.java +++ b/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/WorldGenerators.java @@ -10,7 +10,7 @@ import com.cardinalstar.cubicchunks.util.Mods; import com.cardinalstar.cubicchunks.world.worldgen.compat.DeepslateCubePopulator; import com.cardinalstar.cubicchunks.world.worldgen.noise.OctavesSampler; -import com.cardinalstar.cubicchunks.world.worldgen.noise.ScaledNoise; +import com.cardinalstar.cubicchunks.world.worldgen.noise.ScaledSampler; import com.gtnewhorizon.gtnhlib.util.data.LazyBlock; import cpw.mods.fml.common.Optional; @@ -67,7 +67,7 @@ private static void initEFRPopulation() { public static final DoubleInterval NOODLE_CAVES = new DoubleInterval(0.7, 1); public static final DoubleInterval PILLAR_CAVES = new DoubleInterval(0, 0.3); - public static ScaledNoise caveChooser(Random rng) { - return new ScaledNoise(new OctavesSampler(rng, 2), CHOOSER_SCALE); + public static ScaledSampler caveChooser(Random rng) { + return new ScaledSampler(new OctavesSampler(rng, 2), CHOOSER_SCALE); } } diff --git a/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/caves/NoodleCaveGenerator.java b/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/caves/NoodleCaveGenerator.java index 9e03bf6e..469c887b 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/caves/NoodleCaveGenerator.java +++ b/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/caves/NoodleCaveGenerator.java @@ -1,4 +1,4 @@ -package com.cardinalstar.cubicchunks.world.worldgen.modern; +package com.cardinalstar.cubicchunks.world.worldgen.caves; import java.util.Random; @@ -15,7 +15,7 @@ import com.cardinalstar.cubicchunks.world.worldgen.data.SamplerFactory; import com.cardinalstar.cubicchunks.world.worldgen.noise.NoiseSampler; import com.cardinalstar.cubicchunks.world.worldgen.noise.OctavesSampler; -import com.cardinalstar.cubicchunks.world.worldgen.noise.ScaledNoise; +import com.cardinalstar.cubicchunks.world.worldgen.noise.ScaledSampler; public class NoodleCaveGenerator implements ICubeGenerator { @@ -34,14 +34,14 @@ public NoiseSampler createSampler(Random rng) { @Override public NoiseSampler createSampler(Random rng) { - return new ScaledNoise(new OctavesSampler(rng, 1), SCALE); + return new ScaledSampler(new OctavesSampler(rng, 1), SCALE); } }, B { @Override public NoiseSampler createSampler(Random rng) { - return new ScaledNoise(new OctavesSampler(rng, 2), SCALE); + return new ScaledSampler(new OctavesSampler(rng, 2), SCALE); } }; } diff --git a/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/caves/SpaghettiCaveGenerator.java b/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/caves/SpaghettiCaveGenerator.java index 76d4f4d3..18a123f4 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/caves/SpaghettiCaveGenerator.java +++ b/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/caves/SpaghettiCaveGenerator.java @@ -1,4 +1,4 @@ -package com.cardinalstar.cubicchunks.world.worldgen.modern; +package com.cardinalstar.cubicchunks.world.worldgen.caves; import java.util.Random; @@ -14,7 +14,7 @@ import com.cardinalstar.cubicchunks.world.worldgen.data.SamplerFactory; import com.cardinalstar.cubicchunks.world.worldgen.noise.NoiseSampler; import com.cardinalstar.cubicchunks.world.worldgen.noise.OctavesSampler; -import com.cardinalstar.cubicchunks.world.worldgen.noise.ScaledNoise; +import com.cardinalstar.cubicchunks.world.worldgen.noise.ScaledSampler; public class SpaghettiCaveGenerator implements ICubeGenerator { @@ -26,14 +26,14 @@ private enum Layers implements SamplerFactory { @Override public NoiseSampler createSampler(Random rng) { - return new ScaledNoise(new OctavesSampler(rng, 3), SCALE); + return new ScaledSampler(new OctavesSampler(rng, 3), SCALE); } }, B { @Override public NoiseSampler createSampler(Random rng) { - return new ScaledNoise(new OctavesSampler(rng, 3), SCALE); + return new ScaledSampler(new OctavesSampler(rng, 3), SCALE); } }; } diff --git a/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/BlockNoiseSampler.java b/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/BlockNoiseSampler.java deleted file mode 100644 index 5998b1fd..00000000 --- a/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/BlockNoiseSampler.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.cardinalstar.cubicchunks.world.worldgen.noise; - -public interface BlockNoiseSampler { - - double sample(int x, int y); - - double sample(int x, int y, int z); - -} diff --git a/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/NoiseSampler.java b/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/NoiseSampler.java index a18a9a9e..8bf4190d 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/NoiseSampler.java +++ b/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/NoiseSampler.java @@ -1,9 +1,26 @@ package com.cardinalstar.cubicchunks.world.worldgen.noise; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelBuilder; + +/// A generic noise sampler, typically used for worldgen. +/// The domain of the result MUST BE -1 to 1, but the distribution curve is undefined. +/// The coordinates can be any non-NaN/non-infinity value, positive or negative. +/// [SimplexSampler] will produce a 'boxy' standard distribution. +/// Use a [NormalizedSampler] to convert it into an approximately linear distribution. +/// Use a [ScaledSampler] to automatically multiply the passed-in coordinates. +/// Use an [OctavesSampler] to layer several simplex samplers on top of each other, to increase the complexity and +/// detail of the generated noise. public interface NoiseSampler { double sample(double x, double y); double sample(double x, double y, double z); + default String compileKernel2D(KernelBuilder builder, String x, String y) { + throw new UnsupportedOperationException("Cannot compile NoiseSampler to OpenGL kernel: " + this); + } + + default String compileKernel3D(KernelBuilder builder, String x, String y, String z) { + throw new UnsupportedOperationException("Cannot compile NoiseSampler to OpenGL kernel: " + this); + } } diff --git a/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/NormalizedSampler.java b/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/NormalizedSampler.java new file mode 100644 index 00000000..107da979 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/NormalizedSampler.java @@ -0,0 +1,96 @@ +package com.cardinalstar.cubicchunks.world.worldgen.noise; + +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelBuilder; +import com.cardinalstar.cubicchunks.util.MathUtil; + +/// A sampler that remaps the 'boxy' normal distribution of a [SimplexSampler] to an approximately linear distribution. +public class NormalizedSampler implements NoiseSampler { + + private static final double REMAP_EXPONENT = 0.65; + + private static final int LUT_SIZE = 1024; + + private static final float[] REMAP_LOOKUP_TABLE; + + static { + REMAP_LOOKUP_TABLE = buildLut(REMAP_EXPONENT); + } + + private static float remap(double v) { + v = MathUtil.clamp(v, -1.0, 1.0); + double t = (v + 1.0) * 0.5 * (LUT_SIZE - 1); + int lo = (int) t; + int hi = Math.min(lo + 1, LUT_SIZE - 1); + float frac = (float) (t - lo); + return REMAP_LOOKUP_TABLE[lo] + frac * (REMAP_LOOKUP_TABLE[hi] - REMAP_LOOKUP_TABLE[lo]); + } + + private static float[] buildLut(double exponent) { + float[] lut = new float[LUT_SIZE]; + for (int i = 0; i < LUT_SIZE; i++) { + double v = i / (double) (LUT_SIZE - 1) * 2.0 - 1.0; // map i → [-1, 1] + lut[i] = (float) (Math.signum(v) * Math.pow(Math.abs(v), exponent)); + } + return lut; + } + + private final NoiseSampler base; + + public NormalizedSampler(NoiseSampler base) { + this.base = base; + } + + @Override + public double sample(double x, double y) { + return remap(base.sample(x, y)); + } + + @Override + public double sample(double x, double y, double z) { + return remap(base.sample(x, y, z)); + } + + @Override + public String compileKernel2D(KernelBuilder builder, String x, String y) { + String inner = base.compileKernel2D(builder, x, y); + String clamped = builder.createName("norm_in"); + String result = builder.createName("normalized"); + builder.logic.append(" float ") + .append(clamped) + .append(" = clamp(") + .append(inner) + .append(", -1.0f, 1.0f);\n") + .append(" float ") + .append(result) + .append(" = sign(") + .append(clamped) + .append(") * pow(abs(") + .append(clamped) + .append("), ") + .append((float) REMAP_EXPONENT) + .append("f);\n"); + return result; + } + + @Override + public String compileKernel3D(KernelBuilder builder, String x, String y, String z) { + String inner = base.compileKernel3D(builder, x, y, z); + String clamped = builder.createName("norm_in"); + String result = builder.createName("normalized"); + builder.logic.append(" float ") + .append(clamped) + .append(" = clamp(") + .append(inner) + .append(", -1.0f, 1.0f);\n") + .append(" float ") + .append(result) + .append(" = sign(") + .append(clamped) + .append(") * pow(abs(") + .append(clamped) + .append("), ") + .append((float) REMAP_EXPONENT) + .append("f);\n"); + return result; + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/OctavesSampler.java b/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/OctavesSampler.java index 9e171ea7..d1e836ac 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/OctavesSampler.java +++ b/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/OctavesSampler.java @@ -3,10 +3,16 @@ import java.util.Random; import java.util.function.Supplier; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelBuilder; + +/// Layers several samplers on top of each other. +/// More octaves increase the CPU cost linearly, but increase the complexity and detail of the returned noise. +/// Each octave has an increasing scale (smaller features) and a decreasing amplitude (smaller effect). public class OctavesSampler implements NoiseSampler { private final NoiseSampler[] octaves; private final double[] amplitudes, scales; + private final double norm; public OctavesSampler(Supplier samplers, int octaves) { this.octaves = new NoiseSampler[octaves]; @@ -18,10 +24,18 @@ public OctavesSampler(Supplier samplers, int octaves) { this.amplitudes[i] = 1d / Math.pow(2d, i); this.scales[i] = Math.pow(2d, i); } + + double sum = 0; + + for (double amp : amplitudes) { + sum += amp; + } + + this.norm = 1d / sum; } public OctavesSampler(Random rng, int octaves) { - this(() -> new SimplexNoiseSampler(rng), octaves); + this(() -> new SimplexSampler(rng), octaves); } @Override @@ -35,7 +49,7 @@ public double sample(double x, double y) { value += sampler.sample(x * scale, y * scale) * amplitudes[i]; } - return value; + return value * norm; } @Override @@ -49,6 +63,75 @@ public double sample(double x, double y, double z) { value += sampler.sample(x * scale, y * scale, z * scale) * amplitudes[i]; } - return value; + return value * norm; + } + + @Override + public String compileKernel2D(KernelBuilder builder, String x, String y) { + String result = builder.createName("octaves"); + builder.logic.append(" float ") + .append(result) + .append(" = 0.0f;\n"); + + for (int i = 0, octavesLength = octaves.length; i < octavesLength; i++) { + NoiseSampler sampler = octaves[i]; + double scale = scales[i]; + double amplitude = amplitudes[i]; + + String value = sampler + .compileKernel2D(builder, String.format("(%s) * %ff", x, scale), String.format("(%s) * %ff", y, scale)); + + builder.logic.append(" ") + .append(result) + .append(" += ") + .append(value) + .append(" * ") + .append((float) amplitude) + .append("f;\n"); + } + + builder.logic.append(" ") + .append(result) + .append(" *= ") + .append((float) this.norm) + .append("f;\n"); + + return result; + } + + @Override + public String compileKernel3D(KernelBuilder builder, String x, String y, String z) { + String result = builder.createName("octaves"); + builder.logic.append(" float ") + .append(result) + .append(" = 0.0f;\n"); + + for (int i = 0, octavesLength = octaves.length; i < octavesLength; i++) { + NoiseSampler sampler = octaves[i]; + double scale = scales[i]; + double amplitude = amplitudes[i]; + + String value = sampler.compileKernel3D( + builder, + String.format("(%s) * %ff", x, scale), + String.format("(%s) * %ff", y, scale), + String.format("(%s) * %ff", z, scale)); + + builder.logic.append(" ") + .append(result) + .append(" += ") + .append(value) + .append(" * ") + .append((float) amplitude) + .append("f;\n"); + } + + builder.logic.append(" ") + .append(result) + .append(" *= ") + .append((float) this.norm) + .append("f;\n"); + + return result; } } diff --git a/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/ScaledNoise.java b/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/ScaledNoise.java deleted file mode 100644 index 814422f4..00000000 --- a/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/ScaledNoise.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.cardinalstar.cubicchunks.world.worldgen.noise; - -public class ScaledNoise implements NoiseSampler { - - private final NoiseSampler base; - private final double scaleX; - private final double scaleY; - private final double scaleZ; - - public ScaledNoise(NoiseSampler base, double scaleX, double scaleY, double scaleZ) { - this.base = base; - this.scaleX = scaleX; - this.scaleY = scaleY; - this.scaleZ = scaleZ; - } - - public ScaledNoise(NoiseSampler base, double scale) { - this(base, scale, scale, scale); - } - - @Override - public double sample(double x, double y) { - return base.sample(x * scaleX, y * scaleY); - } - - @Override - public double sample(double x, double y, double z) { - return base.sample(x * scaleX, y * scaleY, z * scaleZ); - } -} diff --git a/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/ScaledSampler.java b/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/ScaledSampler.java new file mode 100644 index 00000000..1b80b01e --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/ScaledSampler.java @@ -0,0 +1,49 @@ +package com.cardinalstar.cubicchunks.world.worldgen.noise; + +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelBuilder; + +/// Scales another sampler by a certain amount in each axis. +/// Effects are the opposite of what you'd expect - scaling by 2 in an axis shrinks the noise by half along that axis. +public class ScaledSampler implements NoiseSampler { + + private final NoiseSampler base; + private final double scaleX; + private final double scaleY; + private final double scaleZ; + + public ScaledSampler(NoiseSampler base, double scaleX, double scaleY, double scaleZ) { + this.base = base; + this.scaleX = scaleX; + this.scaleY = scaleY; + this.scaleZ = scaleZ; + } + + public ScaledSampler(NoiseSampler base, double scale) { + this(base, scale, scale, scale); + } + + @Override + public double sample(double x, double y) { + return base.sample(x * scaleX, y * scaleY); + } + + @Override + public double sample(double x, double y, double z) { + return base.sample(x * scaleX, y * scaleY, z * scaleZ); + } + + @Override + public String compileKernel2D(KernelBuilder builder, String x, String y) { + return base + .compileKernel2D(builder, String.format("(%s) * %ff", x, scaleX), String.format("(%s) * %ff", y, scaleY)); + } + + @Override + public String compileKernel3D(KernelBuilder builder, String x, String y, String z) { + return base.compileKernel3D( + builder, + String.format("(%s) * %ff", x, scaleX), + String.format("(%s) * %ff", y, scaleY), + String.format("(%s) * %ff", z, scaleZ)); + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/SimplexNoiseSampler.java b/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/SimplexSampler.java similarity index 63% rename from src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/SimplexNoiseSampler.java rename to src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/SimplexSampler.java index ea0e87f9..ad29456f 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/SimplexNoiseSampler.java +++ b/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/SimplexSampler.java @@ -4,11 +4,15 @@ import net.minecraft.util.MathHelper; -public class SimplexNoiseSampler implements NoiseSampler { +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelBuilder; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.TransformingBufferAccessor; + +/// A standard simplex noise sampler. +public class SimplexSampler implements NoiseSampler { protected static final int[][] GRADIENTS = new int[][] { { 1, 1, 0 }, { -1, 1, 0 }, { 1, -1, 0 }, { -1, -1, 0 }, { 1, 0, 1 }, { -1, 0, 1 }, { 1, 0, -1 }, { -1, 0, -1 }, { 0, 1, 1 }, { 0, -1, 1 }, { 0, 1, -1 }, { 0, -1, -1 }, - { 1, 1, 0 }, { 0, -1, 1 }, { -1, 1, 0 }, { 0, -1, -1 } }; + { 1, 1, 0 }, { 0, -1, 1 }, { -1, 1, 0 }, { 0, -1, -1 }, }; private static final double SQRT_3 = Math.sqrt(3.0D); private static final double SKEW_FACTOR_2D; private static final double UNSKEW_FACTOR_2D; @@ -17,7 +21,7 @@ public class SimplexNoiseSampler implements NoiseSampler { public final double originY; public final double originZ; - public SimplexNoiseSampler(Random random) { + public SimplexSampler(Random random) { this.originX = random.nextDouble() * 256.0D; this.originY = random.nextDouble() * 256.0D; this.originZ = random.nextDouble() * 256.0D; @@ -65,15 +69,8 @@ public double sample(double x, double y) { double g = (double) j - e; double h = x - f; double k = y - g; - byte n; - byte o; - if (h > k) { - n = 1; - o = 0; - } else { - n = 0; - o = 1; - } + byte n = (byte) (h > k ? 1 : 0); + byte o = (byte) (h > k ? 0 : 1); double p = h - (double) n + UNSKEW_FACTOR_2D; double q = k - (double) o + UNSKEW_FACTOR_2D; @@ -182,4 +179,77 @@ public double sample(double x, double y, double z) { SKEW_FACTOR_2D = 0.5D * (SQRT_3 - 1.0D); UNSKEW_FACTOR_2D = (3.0D - SQRT_3) / 6.0D; } + + @Override + public String compileKernel2D(KernelBuilder builder, String x, String y) { + String funcName = builder.createName("simplex2d"); + + String permsMacro = builder.createName("PERMS"); + + builder.addBufferMacros( + permsMacro, + new TransformingBufferAccessor(builder.addConstant(permutations), index -> "((" + index + ") & 255)")); + + String code = """ + float grad$funcName(int hash, vec2 pos, float distance) { + const int GX[12] = int[12](1,-1,1,-1,1,-1,1,-1,0,0,0,0); + const int GY[12] = int[12](1,1,-1,-1,0,0,0,0,1,-1,1,-1); + + float d = distance - dot(pos, pos); + float d4 = d * d * d * d; + float f = d4 * (float(GX[hash % 12]) * pos.x + float(GY[hash % 12] * pos.y)); + + return d < 0.0f ? 0.0f : f; + } + + float $funcName(float px, float py) { + float d = (px + py) * $skew; + int i = int(floor(px + d)); + int j = int(floor(py + d)); + float e = float(i + j) * $unskew; + float f = float(i) - e; + float g = float(j) - e; + float h = px - f; + float k = py - g; + int n = h > k ? 1 : 0; + int o = h > k ? 0 : 1; + + float p = h - float(n) + $unskew; + float q = k - float(o) + $unskew; + float r = h - 1.0f + 2.0f * $unskew; + float s = k - 1.0f + 2.0f * $unskew; + int t = i & 255; + int u = j & 255; + int v = $perms(t + $perms(u)) % 12; + int w = $perms(t + n + $perms(u + o)) % 12; + int z = $perms(t + 1 + $perms(u + 1)) % 12; + float aa = grad$funcName(v, vec2(h, k), 0.5f); + float ab = grad$funcName(w, vec2(p, q), 0.5f); + float ac = grad$funcName(z, vec2(r. s), 0.5f); + return 70.0f * (aa + ab + ac); + } + """.replaceAll("\\$funcName", funcName) + .replaceAll("\\$skew", Float.toString((float) SKEW_FACTOR_2D)) + .replaceAll("\\$unskew", Float.toString((float) UNSKEW_FACTOR_2D)) + .replaceAll("\\$perms", "GET_" + permsMacro); + + builder.preamble.append(code); + + String result = builder.createName("simplex"); + builder.logic.append(" float ") + .append(result) + .append(" = ") + .append(funcName) + .append("(") + .append(x) + .append(", ") + .append(y) + .append(");\n"); + return result; + } + + @Override + public String compileKernel3D(KernelBuilder builder, String x, String y, String z) { + throw new UnsupportedOperationException("Not yet implemented"); + } } diff --git a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/CCEnhancedWorldGenerator.java b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/CCEnhancedWorldGenerator.java new file mode 100644 index 00000000..44d46648 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/CCEnhancedWorldGenerator.java @@ -0,0 +1,490 @@ +package com.cardinalstar.cubicchunks.worldgen.ccenhanced; + +import static org.lwjgl.vulkan.VK10.vkCmdDispatch; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +import javax.annotation.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; + +import net.minecraft.block.Block; +import net.minecraft.entity.EnumCreatureType; +import net.minecraft.init.Blocks; +import net.minecraft.world.ChunkCoordIntPair; +import net.minecraft.world.ChunkPosition; +import net.minecraft.world.World; +import net.minecraft.world.biome.BiomeGenBase; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.chunk.storage.ExtendedBlockStorage; + +import org.jetbrains.annotations.NotNull; +import org.lwjgl.vulkan.VkCommandBuffer; + +import com.cardinalstar.cubicchunks.api.ICube; +import com.cardinalstar.cubicchunks.api.worldgen.GenerationResult; +import com.cardinalstar.cubicchunks.api.worldgen.IWorldGenerator; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.AcceleratableWorldGenerator; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.ComputePlan; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelBuilder; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelContext; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelExecutor; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.Noise2DKernelExecutor; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.StandardKernelExecutor; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferDataType; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferLayout; +import com.cardinalstar.cubicchunks.mixin.api.ICubicWorldInternal.Server; +import com.cardinalstar.cubicchunks.mixin.ext.EBSIDAccessor; +import com.cardinalstar.cubicchunks.server.CubeProviderServer; +import com.cardinalstar.cubicchunks.server.chunkio.ICubeLoader; +import com.cardinalstar.cubicchunks.util.CubePos; +import com.cardinalstar.cubicchunks.util.Mods; +import com.cardinalstar.cubicchunks.world.core.IColumnInternal; +import com.cardinalstar.cubicchunks.world.cube.Cube; +import com.cardinalstar.cubicchunks.world.worldgen.noise.OctavesSampler; +import com.cardinalstar.cubicchunks.world.worldgen.noise.ScaledSampler; +import com.cardinalstar.cubicchunks.worldgen.ccenhanced.biome.BiomeDistanceKernelExecutor; +import com.cardinalstar.cubicchunks.worldgen.ccenhanced.biome.BiomeLookupKernelExecutor; +import com.cardinalstar.cubicchunks.worldgen.ccenhanced.biome.BiomeLookupResult; +import com.cardinalstar.cubicchunks.worldgen.ccenhanced.biome.CCBiomeGenBase; +import com.cardinalstar.cubicchunks.worldgen.ccenhanced.biome.CCBiomeRegistry; +import com.cardinalstar.cubicchunks.worldgen.ccenhanced.biome.HeightMapKernelExecutor; +import com.cardinalstar.cubicchunks.worldgen.ccenhanced.climate.ClimateSystem; +import com.cardinalstar.cubicchunks.worldgen.ccenhanced.surface.CCSurfacePainter; +import com.cardinalstar.cubicchunks.worldgen.ccenhanced.terrain.CCBiomeCache; +import com.cardinalstar.cubicchunks.worldgen.ccenhanced.terrain.CCTerrainGenerator; +import com.cardinalstar.cubicchunks.worldgen.ccenhanced.terrain.ColumnContext; +import com.falsepattern.endlessids.mixin.helpers.ChunkBiomeHook; +import com.google.common.collect.ImmutableMap; +import com.gtnewhorizon.gtnhlib.hash.Fnv1a64; + +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; + +@ParametersAreNonnullByDefault +public class CCEnhancedWorldGenerator implements IWorldGenerator, AcceleratableWorldGenerator { + + /** ColumnContext LRU cache capacity: enough to cover a generous chunk-loading radius. */ + private static final int CONTEXT_CACHE_CAPACITY = 256; + + private final World world; + private final ClimateSystem climateSystem; + private final CCBiomeCache biomeCache; + private final CCTerrainGenerator terrainGen; + private final CCSurfacePainter surfacePainter; + + // LRU cache: column position key → ColumnContext computed during provideColumn, + // read back during provideCube calls for the same column. + private final LinkedHashMap contextCache; + + public CCEnhancedWorldGenerator(World world) { + this.world = world; + long seed = world.getSeed(); + this.climateSystem = new ClimateSystem(seed); + this.biomeCache = new CCBiomeCache(climateSystem); + this.terrainGen = new CCTerrainGenerator(seed); + this.surfacePainter = new CCSurfacePainter(); + this.contextCache = new LinkedHashMap<>(CONTEXT_CACHE_CAPACITY, 0.75f, true) { + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > CONTEXT_CACHE_CAPACITY; + } + }; + } + + private @NotNull ICubeLoader getCubeLoader() { + return getCubeProvider().getCubeLoader(); + } + + private CubeProviderServer getCubeProvider() { + return ((Server) this.world).getCubeCache(); + } + + // ------------------------------------------------------------------------- + // Column + // ------------------------------------------------------------------------- + + @Override + public GenerationResult provideColumn(World world, int columnX, int columnZ) { + Chunk chunk = new Chunk(world, columnX, columnZ); + + ColumnContext ctx = getOrComputeContext(chunk, columnX, columnZ); + + BiomeLookupResult[] grid = biomeCache.getGrid(columnX, columnZ); + // fillBiomeArray(chunk, grid); + + ExtendedBlockStorage[] ebsArr = new ExtendedBlockStorage[16]; + for (int cubeY = 0; cubeY < 16; cubeY++) { + ebsArr[cubeY] = terrainGen.buildEbs(world, ctx, cubeY); + } + + surfacePainter.paint(world, ebsArr, ctx, grid); + + List cubes = new ArrayList<>(16); + for (int cubeY = 0; cubeY < 16; cubeY++) { + cubes.add(new Cube(chunk, cubeY, ebsArr[cubeY])); + } + + return new GenerationResult<>(chunk, null, cubes); + } + + // ------------------------------------------------------------------------- + // Cube + // ------------------------------------------------------------------------- + + @Override + public GenerationResult provideCube(@Nullable Chunk chunk, int cubeX, int cubeY, int cubeZ) { + List generatedColumns = new ArrayList<>(); + List generatedCubes = new ArrayList<>(); + + // Generate column + all vanilla-range cubes if chunk is missing or + // the requested cube falls in the vanilla range. + if ((cubeY >= 0 && cubeY < 16) || chunk == null) { + if (chunk == null) { + chunk = new Chunk(world, cubeX, cubeZ); + ((IColumnInternal) chunk).setColumn(true); + generatedColumns.add(chunk); + } + ColumnContext ctx = getOrComputeContext(chunk, cubeX, cubeZ); + + BiomeLookupResult[] grid = biomeCache.getGrid(cubeX, cubeZ); + // fillBiomeArray(chunk, grid); + + ExtendedBlockStorage[] ebsArr = new ExtendedBlockStorage[16]; + for (int cy = 0; cy < 16; cy++) { + ebsArr[cy] = terrainGen.buildEbs(world, ctx, cy); + } + + surfacePainter.paint(world, ebsArr, ctx, grid); + + for (int cy = 0; cy < 16; cy++) { + generatedCubes.add(new Cube(chunk, cy, ebsArr[cy])); + } + } + + // Cubes outside [0, 16) are generated individually using the same terrain rules. + if (cubeY < 0 || cubeY >= 16) { + ColumnContext ctx = getOrComputeContext(chunk, cubeX, cubeZ); + generatedCubes.add(new Cube(chunk, cubeY, terrainGen.buildEbs(world, ctx, cubeY))); + } + + // Extract the requested cube as the primary result. + Cube primary = null; + for (int i = 0; i < generatedCubes.size(); i++) { + Cube c = generatedCubes.get(i); + if (c.getY() == cubeY) { + primary = c; + generatedCubes.remove(i); + break; + } + } + + return new GenerationResult<>(primary, generatedColumns, generatedCubes); + } + + // ------------------------------------------------------------------------- + // Population + // ------------------------------------------------------------------------- + + @Override + public void populate(Cube cube) { + cube.markPopulated(Cube.POP_ALL); + } + + // ------------------------------------------------------------------------- + // Structure stubs + // ------------------------------------------------------------------------- + + @Override + public void recreateStructures(ICube cube) {} + + @Override + public void recreateStructures(Chunk column) {} + + @Override + public List getPossibleCreatures(EnumCreatureType type, int x, int y, int z) { + return BiomeGenBase.plains.getSpawnableList(type); + } + + @Override + @Nullable + public ChunkPosition getNearestStructure(String name, int x, int y, int z) { + return null; + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + /** Returns the cached ColumnContext for this column, computing it on demand if absent. */ + private ColumnContext getOrComputeContext(Chunk chunk, int chunkX, int chunkZ) { + long key = packKey(chunkX, chunkZ); + ColumnContext ctx = contextCache.get(key); + if (ctx == null) { + ctx = terrainGen.computeColumnContext(chunk, chunkX, chunkZ, biomeCache); + contextCache.put(key, ctx); + } + return ctx; + } + + /** + * Writes the primary biome ID for each block column into the chunk's biome array + * (indexed as bz<<4|bx), so the game reports correct biome names and spawning. + */ + private static void fillBiomeArray(Chunk chunk, BiomeLookupResult[] grid) { + byte[] biomeArray = chunk.getBiomeArray(); + + for (int bx = 0; bx < 16; bx++) { + for (int bz = 0; bz < 16; bz++) { + CCBiomeGenBase biome = grid[bz << 4 | bx].primary; + biomeArray[bz << 4 | bx] = (byte) (biome.biomeID & 0xFF); + } + } + } + + private static long packKey(int chunkX, int chunkZ) { + return ((long) chunkX << 32) | (chunkZ & 0xFFFFFFFFL); + } + + private boolean kernelsInitialized; + private KernelExecutor tempKernel; + private KernelExecutor humidityKernel; + private KernelExecutor contKernel; + private KernelExecutor erosionKernel; + private BiomeDistanceKernelExecutor distancesKernel; + private BiomeLookupKernelExecutor lookupKernel; + private Noise2DKernelExecutor hvNoiseKernel; + private HeightMapKernelExecutor heightMapKernel; + private BlockGenKernelExecutor blockKernel; + + @Override + public ComputePlan plan(@Nullable Chunk column, int columnX, int columnZ, IntArrayList cubeYLevels) { + if (!kernelsInitialized) { + long worldSeed = world.getSeed(); + long hvSeed = Fnv1a64.hashStep(Fnv1a64.hashStep(Fnv1a64.initialState(), worldSeed), 100L); + List biomes = CCBiomeRegistry.getBiomes(); + + kernelsInitialized = true; + + tempKernel = climateSystem.createAxisKernel(ClimateSystem.TEMPERATURE); + humidityKernel = climateSystem.createAxisKernel(ClimateSystem.HUMIDITY); + contKernel = climateSystem.createAxisKernel(ClimateSystem.CONTINENTALNESS); + erosionKernel = climateSystem.createAxisKernel(ClimateSystem.EROSION); + + distancesKernel = new BiomeDistanceKernelExecutor(biomes.toArray(new CCBiomeGenBase[0])); + lookupKernel = new BiomeLookupKernelExecutor(biomes.size()); + + hvNoiseKernel = new Noise2DKernelExecutor( + new ScaledSampler(new OctavesSampler(new Random(hvSeed), 4), 1.0 / 400.0)); + + heightMapKernel = new HeightMapKernelExecutor(biomes); + blockKernel = new BlockGenKernelExecutor(); + + KernelContext.getScheduler() + .compileExecutor(tempKernel); + KernelContext.getScheduler() + .compileExecutor(humidityKernel); + KernelContext.getScheduler() + .compileExecutor(contKernel); + KernelContext.getScheduler() + .compileExecutor(erosionKernel); + KernelContext.getScheduler() + .compileExecutor(distancesKernel); + KernelContext.getScheduler() + .compileExecutor(lookupKernel); + KernelContext.getScheduler() + .compileExecutor(hvNoiseKernel); + KernelContext.getScheduler() + .compileExecutor(heightMapKernel); + KernelContext.getScheduler() + .compileExecutor(blockKernel); + } + + ComputePlan plan = new ComputePlan(); + + // Steps 1–3: four per-axis climate kernels write into one shared buffer, then distances → lookup + ChunkCoordIntPair chunkCoord = new ChunkCoordIntPair(columnX, columnZ); + + var temperature = plan.submit(tempKernel, chunkCoord) + .get("noise"); + var humidity = plan.submit(humidityKernel, chunkCoord) + .get("noise"); + var continentalness = plan.submit(contKernel, chunkCoord) + .get("noise"); + var erosion = plan.submit(erosionKernel, chunkCoord) + .get("noise"); + + var distances = plan.submit( + distancesKernel, + ImmutableMap.of( + "temperature", + temperature, + "humidity", + humidity, + "continentalness", + continentalness, + "erosion", + erosion)); + + // var lookup = plan.submit(lookupKernel, ImmutableMap.of("distance", distances.get("distance"))); + + // Step 4: HV noise (independent of climate pipeline) + var hvNoise = plan.submit(hvNoiseKernel, chunkCoord); + + // Step 5: height map — blends all biomes via Gaussian falloff over raw distances, + // avoiding top-N membership discontinuities at biome boundaries. + var heightData = plan.submit( + heightMapKernel, + ImmutableMap.of("distance", distances.get("distance"), "hvNoise", hvNoise.get("noise"))); + + IntOpenHashSet cubesToGenerate = new IntOpenHashSet(cubeYLevels); + + if (column == null) { + column = new Chunk(world, columnX, columnZ); + + Chunk column2 = column; + + plan.terminal(ImmutableMap.of(), inputs -> this.processColumn(inputs, column2)); + + // Generate all terrain-range cubes atomically to prevent sync-path side-effect conflicts. + // provideCube generates y∈[0,15] as side effects for any request in that range. + for (int y = 0; y < 16; y++) { + cubesToGenerate.add(y); + } + } + + for (Integer cubeY : cubesToGenerate) { + var blockData = plan.submit( + blockKernel, + new CubePos(columnX, cubeY, columnZ), + ImmutableMap.of("height", heightData.get("height"))); + + Cube cube = new Cube(column, cubeY); + + plan.terminal(blockData, inputs -> this.processCube(inputs, cube)); + } + + return plan; + } + + private void processColumn(Map inputs, Chunk column) { + // var biomes = inputs.get("closestBiomes").asIntBuffer(); + // + // for (int z = 0; z < 16; z++) { + // for (int x = 0; x < 16; x++) { + // int biomeIndex = biomes.get(((z << 4) | x) * 4); + // + // CCBiomeGenBase biome = CCBiomeRegistry.getBiome(biomeIndex); + // + // putBiome(column, x, z, biome); + // } + // } + + getCubeLoader().addColumn(column); + } + + private static void putBiome(Chunk column, int x, int z, CCBiomeGenBase biome) { + if (Mods.EndlessIDs.isModLoaded()) { + var biomes = ((ChunkBiomeHook) column).getBiomeShortArray(); + + biomes[(z << 4) | x] = (short) biome.biomeID; + } else { + var biomes = column.getBiomeArray(); + + biomes[(z << 4) | x] = (byte) biome.biomeID; + } + } + + private void processCube(Map inputs, Cube cube) { + var rawBlockData = inputs.get("block") + .asIntBuffer(); + + ExtendedBlockStorage ebs = cube.getOrCreateStorage(); + + EBSIDAccessor ids = (EBSIDAccessor) ebs; + + int stone = Block.getIdFromBlock(Blocks.stone); + int dirt = Block.getIdFromBlock(Blocks.dirt); + int grass = Block.getIdFromBlock(Blocks.grass); + int log = Block.getIdFromBlock(Blocks.log); + + for (int z = 0; z < 16; z++) { + for (int y = 0; y < 16; y++) { + for (int x = 0; x < 16; x++) { + int block = rawBlockData.get((z << 8) | (y << 4) | x); + + if (block == 0) continue; + + ids.setBlockID(x, y, z, switch (block) { + case 1 -> stone; + case 2 -> dirt; + case 3 -> grass; + default -> log; + }, block == 3); + } + } + } + + getCubeLoader().addCube(cube); + } + + private static class BlockGenKernelExecutor extends StandardKernelExecutor { + + @Override + protected String generateKernel(KernelBuilder builder) { + builder.addParameter(BufferDataType.i32, "cubeY"); + + builder.addInputBuffer("height", new BufferLayout(BufferDataType.f32, 16, 16)); + + builder.addOutputBuffer("block", new BufferLayout(BufferDataType.u32, 16, 16, 16)); + + return """ + #version 460 + + layout(local_size_x = 16, local_size_y = 16) in; + + layout(set = 0, binding = 0) readonly buffer Constants { uint constants[]; }; + layout(set = 1, binding = 0) buffer Arena { uint arena[]; }; + + $preamble + + $pc + + void main() { + uint x = gl_GlobalInvocationID.x; + uint y = gl_GlobalInvocationID.y; + uint z = gl_GlobalInvocationID.z; + + uint blockColumn = (z << 4) | x; + + int gy = int(y) + (GET_CUBE_Y << 4); + + int topBlock = 60; + + uint block = 0; // air + + block = gy < topBlock ? 2 : block; // dirt + block = gy < topBlock - 3 ? 1 : block; // stone overrides dirt for deep blocks + block = gy == topBlock ? 3 : block; // grass + + SET_BLOCK(((z << 8u) | (y << 4u) | x), z > 0 ? z + 1 : block); + } + """; + } + + @Override + protected Map getParameters(CubePos cubePos) { + return ImmutableMap.of("cubeY", cubePos.getY()); + } + + @Override + protected void dispatch(VkCommandBuffer commands) { + vkCmdDispatch(commands, 1, 1, 16); + } + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/CCEnhancedWorldType.java b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/CCEnhancedWorldType.java new file mode 100644 index 00000000..16b15a74 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/CCEnhancedWorldType.java @@ -0,0 +1,44 @@ +package com.cardinalstar.cubicchunks.worldgen.ccenhanced; + +import javax.annotation.ParametersAreNonnullByDefault; + +import net.minecraft.world.World; +import net.minecraft.world.WorldServer; +import net.minecraft.world.WorldType; + +import org.jetbrains.annotations.NotNull; + +import com.cardinalstar.cubicchunks.api.IntRange; +import com.cardinalstar.cubicchunks.api.world.ICubicWorldType; +import com.cardinalstar.cubicchunks.api.worldgen.IWorldGenerator; +import com.cardinalstar.cubicchunks.worldgen.ccenhanced.biome.defaults.CCBiomes; + +@ParametersAreNonnullByDefault +public class CCEnhancedWorldType extends WorldType implements ICubicWorldType { + + public static CCEnhancedWorldType INSTANCE; + + private CCEnhancedWorldType() { + super("Cubic Chunks"); + } + + public static void init() { + INSTANCE = new CCEnhancedWorldType(); + CCBiomes.init(); + } + + @Override + public @NotNull IWorldGenerator createCubeGenerator(World world) { + return new CCEnhancedWorldGenerator(world); + } + + @Override + public IntRange calculateGenerationHeightRange(WorldServer world) { + return new IntRange(-1024, 1024); + } + + @Override + public boolean hasCubicGeneratorForWorld(World world) { + return true; + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/BiomeDistanceKernelExecutor.java b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/BiomeDistanceKernelExecutor.java new file mode 100644 index 00000000..4493e188 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/BiomeDistanceKernelExecutor.java @@ -0,0 +1,72 @@ +package com.cardinalstar.cubicchunks.worldgen.ccenhanced.biome; + +import static org.lwjgl.vulkan.VK10.vkCmdDispatch; + +import org.lwjgl.vulkan.VkCommandBuffer; + +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelBuilder; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.StandardKernelExecutor; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferDataType; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferLayout; + +public class BiomeDistanceKernelExecutor extends StandardKernelExecutor { + + @org.jetbrains.annotations.NotNull + private final CCBiomeGenBase[] biomes; + + public BiomeDistanceKernelExecutor(CCBiomeGenBase[] biomes) { + this.biomes = biomes; + } + + @Override + protected String generateKernel(KernelBuilder builder) { + float[] climatePoints = new float[biomes.length * 4]; + + for (int i = 0; i < biomes.length; i++) { + System.arraycopy(biomes[i].climatePoint, 0, climatePoints, i * 4, 4); + } + + builder.addBufferMacros("BIOME_COORDS", builder.addConstant(climatePoints)); + + builder.addInputBuffer("temperature", new BufferLayout(BufferDataType.f32, 16, 16)); + builder.addInputBuffer("humidity", new BufferLayout(BufferDataType.f32, 16, 16)); + builder.addInputBuffer("continentalness", new BufferLayout(BufferDataType.f32, 16, 16)); + builder.addInputBuffer("erosion", new BufferLayout(BufferDataType.f32, 16, 16)); + + builder.addOutputBuffer("distance", new BufferLayout(BufferDataType.f32, 16, 16, biomes.length)); + + return """ + #version 460 + + layout(local_size_x = 16, local_size_y = 16) in; + + layout(set = 0, binding = 0) readonly buffer Constants { uint constants[]; }; + layout(set = 1, binding = 0) buffer Arena { uint arena[]; }; + + $preamble + + $pc + + void main() { + uint x = gl_GlobalInvocationID.x; + uint y = gl_GlobalInvocationID.y; + uint biome = gl_GlobalInvocationID.z; + + uint pixelIndex = y * 16u + x; + uint biomeIndex = biome * 4u; + + float a = GET_TEMPERATURE(pixelIndex) - GET_BIOME_COORDS(biomeIndex + 0u); + float b = GET_HUMIDITY(pixelIndex) - GET_BIOME_COORDS(biomeIndex + 1u); + float c = GET_CONTINENTALNESS(pixelIndex) - GET_BIOME_COORDS(biomeIndex + 2u); + float d = GET_EROSION(pixelIndex) - GET_BIOME_COORDS(biomeIndex + 3u); + + SET_DISTANCE(biome * 256u + pixelIndex, sqrt(a * a + b * b + c * c + d * d)); + } + """; + } + + @Override + protected void dispatch(VkCommandBuffer commands) { + vkCmdDispatch(commands, 1, 1, biomes.length); + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/BiomeLookupKernelExecutor.java b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/BiomeLookupKernelExecutor.java new file mode 100644 index 00000000..57914fe5 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/BiomeLookupKernelExecutor.java @@ -0,0 +1,82 @@ +package com.cardinalstar.cubicchunks.worldgen.ccenhanced.biome; + +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelBuilder; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.StandardKernelExecutor; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferDataType; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferLayout; + +public class BiomeLookupKernelExecutor extends StandardKernelExecutor { + + private final int biomeCount; + + public BiomeLookupKernelExecutor(int biomeCount) { + this.biomeCount = biomeCount; + } + + @Override + protected String generateKernel(KernelBuilder builder) { + builder.addInputBuffer("distance", new BufferLayout(BufferDataType.f32, 16, 16, biomeCount)); + + builder.addOutputBuffer("closestBiome", new BufferLayout(BufferDataType.i32, 16, 16, 4)); + builder.addOutputBuffer("biomeWeight", new BufferLayout(BufferDataType.f32, 16, 16, 4)); + + builder.addMacro("BIOME_COUNT", biomeCount); + + return """ + #version 460 + + layout(local_size_x = 16, local_size_y = 16) in; + + layout(set = 0, binding = 0) readonly buffer Constants { uint constants[]; }; + layout(set = 1, binding = 0) buffer Arena { uint arena[]; }; + + $preamble + + $pc + + const uint BIOME_STRIDE = 16u * 16u; + + void main() { + uint x = gl_GlobalInvocationID.x; + uint y = gl_GlobalInvocationID.y; + + uint columnOffset = (y << 4u) | x; + + // Single pass: maintain top-4 nearest in registers via insertion sort. + float dist[4] = float[4](1.0e30f, 1.0e30f, 1.0e30f, 1.0e30f); + int idx[4] = int[4](-1, -1, -1, -1); + + for (uint i = 0u; i < uint(BIOME_COUNT); i++) { + float value = GET_DISTANCE(BIOME_STRIDE * i + columnOffset); + + if (value < dist[3]) { + dist[3] = value; idx[3] = int(i); + if (dist[2] > dist[3]) { float t = dist[2]; dist[2] = dist[3]; dist[3] = t; int ti = idx[2]; idx[2] = idx[3]; idx[3] = ti; } + if (dist[1] > dist[2]) { float t = dist[1]; dist[1] = dist[2]; dist[2] = t; int ti = idx[1]; idx[1] = idx[2]; idx[2] = ti; } + if (dist[0] > dist[1]) { float t = dist[0]; dist[0] = dist[1]; dist[1] = t; int ti = idx[0]; idx[0] = idx[1]; idx[1] = ti; } + } + } + + // Write closest biome indices + SET_CLOSEST_BIOME(columnOffset * 4u + 0u, uint(idx[0])); + SET_CLOSEST_BIOME(columnOffset * 4u + 1u, uint(idx[1])); + SET_CLOSEST_BIOME(columnOffset * 4u + 2u, uint(idx[2])); + SET_CLOSEST_BIOME(columnOffset * 4u + 3u, uint(idx[3])); + + // Weights are inverse distance — closer biome gets higher weight. + float invA = 1.0f / max(dist[0], 1e-6f); + float invB = 1.0f / max(dist[1], 1e-6f); + float invC = 1.0f / max(dist[2], 1e-6f); + float invD = 1.0f / max(dist[3], 1e-6f); + + float norm = 1.0f / (invA + invB + invC + invD); + + // Write biome weights + SET_BIOME_WEIGHT(columnOffset * 4u + 0u, invA * norm); + SET_BIOME_WEIGHT(columnOffset * 4u + 1u, invB * norm); + SET_BIOME_WEIGHT(columnOffset * 4u + 2u, invC * norm); + SET_BIOME_WEIGHT(columnOffset * 4u + 3u, invD * norm); + } + """; + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/BiomeLookupResult.java b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/BiomeLookupResult.java new file mode 100644 index 00000000..955c4145 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/BiomeLookupResult.java @@ -0,0 +1,39 @@ +package com.cardinalstar.cubicchunks.worldgen.ccenhanced.biome; + +/** + * Result of a Voronoi biome lookup: the top-N nearest biomes in climate space + * along with their inverse-distance-squared blend weights. + */ +public class BiomeLookupResult { + + /** The single nearest biome in climate space (== neighbors[0]). */ + public final CCBiomeGenBase primary; + /** Top-N nearest biomes sorted by climate distance; index 0 is the primary (closest). */ + public final CCBiomeGenBase[] neighbors; + /** Blend weights corresponding to neighbors[], normalised to sum to 1. */ + public final float[] weights; + + public BiomeLookupResult(CCBiomeGenBase primary, CCBiomeGenBase[] neighbors, float[] weights) { + this.primary = primary; + this.neighbors = neighbors; + this.weights = weights; + } + + /** + * Compute the blended value of a float field across all neighbors. + * Usage: {@code result.blend(b -> b.rootHeight)} + */ + public float blend(BiomeFloatGetter getter) { + float sum = 0; + for (int i = 0; i < neighbors.length; i++) { + sum += weights[i] * getter.get(neighbors[i]); + } + return sum; + } + + @FunctionalInterface + public interface BiomeFloatGetter { + + float get(CCBiomeGenBase biome); + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/CCBiomeGenBase.java b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/CCBiomeGenBase.java new file mode 100644 index 00000000..523bb47e --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/CCBiomeGenBase.java @@ -0,0 +1,59 @@ +package com.cardinalstar.cubicchunks.worldgen.ccenhanced.biome; + +import net.minecraft.init.Blocks; +import net.minecraft.world.biome.BiomeGenBase; + +import com.gtnewhorizon.gtnhlib.util.data.BlockMeta; +import com.gtnewhorizon.gtnhlib.util.data.ImmutableBlockMeta; + +/** + * Base class for CC Enhanced biomes. + * + *

+ * Extends BiomeGenBase and adds: + *

    + *
  • A 4D climate point for Voronoi biome lookup (temperature, humidity, continentalness, erosion)
  • + *
  • ImmutableBlockMeta surface block fields (shadow the plain Block fields on BiomeGenBase)
  • + *
  • Additional terrain-shaping parameters (perturbationScale, fillerDepth)
  • + *
  • Carver eligibility flags (allowVillage, allowCanyons, allowTrenches)
  • + *
+ * + *

+ * Vanilla fields rootHeight and heightVariation (inherited from BiomeGenBase) are reused + * as terrain shaping parameters in the heightmap formula. + */ +public class CCBiomeGenBase extends BiomeGenBase { + + /** Position in climate space. Axis order: [temperature, humidity, continentalness, erosion]. */ + public final float[] climatePoint; + + // --- Carver / structure flags --- + public boolean allowVillage = false; + public boolean allowCanyons = false; + public boolean allowTrenches = false; + + // --- Surface block fields (ImmutableBlockMeta shadows BiomeGenBase.topBlock/fillerBlock) --- + /** Block placed at the topmost solid layer (e.g. grass, sand). */ + public ImmutableBlockMeta ccTopBlock = new BlockMeta(Blocks.grass, 0); + /** Block placed in the filler layer directly below the top block (e.g. dirt). */ + public ImmutableBlockMeta ccFillerBlock = new BlockMeta(Blocks.dirt, 0); + /** Block used for all solid blocks below the filler layer (e.g. stone). */ + public ImmutableBlockMeta ccStoneBlock = new BlockMeta(Blocks.stone, 0); + + // --- Extra terrain parameters --- + /** Multiplier on the 3D perturbation noise amplitude. >1 = more overhangs/roughness. */ + public float perturbationScale = 1.0f; + /** How many blocks below the top block receive ccFillerBlock. */ + public int fillerDepth = 4; + + /** + * @param biomeId Biome registry ID (0–255); registered in BiomeGenBase.biomeList + * @param name Human-readable biome name + * @param climatePoint 4D climate position [temperature, humidity, continentalness, erosion] + */ + public CCBiomeGenBase(int biomeId, String name, float[] climatePoint) { + super(biomeId); + setBiomeName(name); + this.climatePoint = climatePoint; + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/CCBiomeRegistry.java b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/CCBiomeRegistry.java new file mode 100644 index 00000000..40241cda --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/CCBiomeRegistry.java @@ -0,0 +1,101 @@ +package com.cardinalstar.cubicchunks.worldgen.ccenhanced.biome; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Registry for CC Enhanced biomes. Supports Voronoi nearest-neighbour lookup in 4D climate space. + * + *

+ * All biomes must be registered via {@link #register} during FML initialisation (preInit) + * before any world creation, per FR-03.6. + */ +public class CCBiomeRegistry { + + private static final List biomes = new ArrayList<>(); + + public static void register(CCBiomeGenBase biome) { + biomes.add(biome); + } + + public static CCBiomeGenBase getBiome(int index) { + return biomes.get(index); + } + + public static List getBiomes() { + return Collections.unmodifiableList(biomes); + } + + /** + * Voronoi nearest-neighbour lookup in 4D climate space. O(N) in biome count. + * + *

+ * Returns the top-n biomes by Euclidean distance to {@code climatePoint}, with + * blend weights proportional to inverse-distance-squared, normalised to sum 1. + * + * @param climatePoint Query point: [temperature, humidity, continentalness, erosion] + * @param n Number of nearest biomes to return (clamped to biome count) + */ + public static BiomeLookupResult lookup(float[] climatePoint, int n) { + if (biomes.isEmpty()) throw new IllegalStateException("No biomes registered in CCBiomeRegistry"); + n = Math.min(n, biomes.size()); + + // Compute squared distances to all registered biomes + float[] dists = new float[biomes.size()]; + for (int i = 0; i < biomes.size(); i++) { + dists[i] = distanceSq(climatePoint, biomes.get(i).climatePoint); + } + + // Find indices of the n smallest distances, sorted closest-first + int[] indices = topNSorted(dists, n); + + // Compute inverse-distance-squared weights, normalised + float[] weights = new float[n]; + float sum = 0; + for (int i = 0; i < n; i++) { + weights[i] = 1.0f / (dists[indices[i]] + 1e-6f); + sum += weights[i]; + } + for (int i = 0; i < n; i++) weights[i] /= sum; + + CCBiomeGenBase[] neighbors = new CCBiomeGenBase[n]; + for (int i = 0; i < n; i++) neighbors[i] = biomes.get(indices[i]); + + return new BiomeLookupResult(neighbors[0], neighbors, weights); + } + + /** + * Squared Euclidean distance in climate space. + * Uses the shorter array length to tolerate biomes with fewer axes than the current system. + */ + static float distanceSq(float[] a, float[] b) { + float d = 0; + int len = Math.min(a.length, b.length); + for (int i = 0; i < len; i++) { + float diff = a[i] - b[i]; + d += diff * diff; + } + return d; + } + + /** + * Returns the indices of the n entries with the smallest values in {@code dists}, + * sorted in ascending order (index 0 = smallest distance = primary biome). + */ + private static int[] topNSorted(float[] dists, int n) { + int[] indices = new int[dists.length]; + + for (int i = 0; i < dists.length; i++) indices[i] = i; + + it.unimi.dsi.fastutil.Arrays + .mergeSort(0, dists.length, (a, b) -> Float.compare(dists[indices[a]], dists[indices[b]]), (a, b) -> { + int temp = indices[a]; + indices[a] = indices[b]; + indices[b] = temp; + }); + + return Arrays.copyOf(indices, n); + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/HeightMapKernelExecutor.java b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/HeightMapKernelExecutor.java new file mode 100644 index 00000000..02be7c7e --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/HeightMapKernelExecutor.java @@ -0,0 +1,101 @@ +package com.cardinalstar.cubicchunks.worldgen.ccenhanced.biome; + +import java.util.List; + +import org.jetbrains.annotations.NotNull; + +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelBuilder; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.StandardKernelExecutor; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferDataType; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferLayout; + +/** + * Blends per-biome rootHeight and heightVariation using Gaussian falloff over all biomes in + * 4D climate space, then modulates the result with a height-variation noise layer. + * + *

+ * Weight for biome i = exp(-FALLOFF * dist_i²), normalized across all biomes. + * Using all biomes (not a top-N subset) ensures no discontinuity when the nearest-biome set + * changes between adjacent columns. + * + *

+ * Output: heightMap[col] = blendedRoot + blendedVar * hvNoise[col]. + */ +public class HeightMapKernelExecutor extends StandardKernelExecutor { + + /** + * Controls how sharply weight falls off with climate-space distance. + * Higher values make transitions crisper; lower values blend further across biome space. + * Tune alongside the climate axis frequency scales in + * {@link com.cardinalstar.cubicchunks.worldgen.ccenhanced.climate.ClimateSystem}. + */ + private static final float FALLOFF = 10.0f; + + @NotNull + private final List biomes; + + /** Embeds per-biome rootHeight and heightVariation as GLSL constant arrays. */ + public HeightMapKernelExecutor(List biomes) { + this.biomes = biomes; + } + + @Override + protected String generateKernel(KernelBuilder builder) { + float[] rootHeight = new float[biomes.size()]; + float[] heightVariation = new float[biomes.size()]; + + for (int i = 0; i < biomes.size(); i++) { + CCBiomeGenBase b = biomes.get(i); + + rootHeight[i] = b.rootHeight; + heightVariation[i] = b.heightVariation; + } + + builder.addBufferMacros("ROOT_HEIGHT", builder.addConstant(rootHeight)); + builder.addBufferMacros("HEIGHT_VARIATION", builder.addConstant(heightVariation)); + builder.addMacro("FALLOFF", FALLOFF); + builder.addMacro("BIOME_COUNT", biomes.size()); + + builder.addInputBuffer("distance", new BufferLayout(BufferDataType.f32, 16, 16, biomes.size())); + builder.addInputBuffer("hvNoise", new BufferLayout(BufferDataType.f32, 16, 16)); + + builder.addOutputBuffer("height", new BufferLayout(BufferDataType.f32, 16, 16)); + + return """ + #version 460 + + layout(local_size_x = 16, local_size_y = 16) in; + + layout(set = 0, binding = 0) readonly buffer Constants { uint constants[]; }; + layout(set = 1, binding = 0) buffer Arena { uint arena[]; }; + + $preamble + + $pc + + void main() { + uint x = gl_GlobalInvocationID.x; + uint y = gl_GlobalInvocationID.y; + + uint columnOffset = y << 4u | x; + + float blendedRoot = 0.0f; + float blendedVar = 0.0f; + float totalWeight = 0.0f; + + for (uint biome = 0u; biome < uint(BIOME_COUNT); biome++) { + float dist = GET_DISTANCE(biome * 256u + columnOffset); + float w = exp(-FALLOFF * dist * dist); + blendedRoot += GET_ROOT_HEIGHT(biome) * w; + blendedVar += GET_HEIGHT_VARIATION(biome) * w; + totalWeight += w; + } + + blendedRoot /= totalWeight; + blendedVar /= totalWeight; + + SET_HEIGHT(columnOffset, blendedRoot + blendedVar * GET_HV_NOISE(columnOffset)); + } + """; + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/defaults/CCBiomes.java b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/defaults/CCBiomes.java new file mode 100644 index 00000000..8c0da865 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/defaults/CCBiomes.java @@ -0,0 +1,118 @@ +package com.cardinalstar.cubicchunks.worldgen.ccenhanced.biome.defaults; + +import net.minecraft.init.Blocks; + +import com.cardinalstar.cubicchunks.worldgen.ccenhanced.biome.CCBiomeGenBase; +import com.cardinalstar.cubicchunks.worldgen.ccenhanced.biome.CCBiomeRegistry; +import com.gtnewhorizon.gtnhlib.color.HSVColor; +import com.gtnewhorizon.gtnhlib.util.data.BlockMeta; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; + +/** + * Default CC Enhanced biome set. 12 biomes covering the full climate space. + * Climate point axes: [temperature, humidity, continentalness, erosion]. + * + *

+ * Biome IDs 200–211 are reserved for this mod. rootHeight and heightVariation + * follow the formula in SDD §4.3: + * {@code surfaceY = SEA_LEVEL + rootHeight * 64 + heightVariation * hvNoise * 64}. + */ +public final class CCBiomes { + + // Biome ID range: 200–211 + private static final int ID_DEEP_OCEAN = 200; + private static final int ID_OCEAN = 201; + private static final int ID_BEACH = 202; + private static final int ID_PLAINS = 203; + private static final int ID_ROLLING_HILLS = 204; + private static final int ID_FOREST = 205; + private static final int ID_LARGE_HILLS = 206; + private static final int ID_MOUNTAINS = 207; + private static final int ID_DESERT = 208; + private static final int ID_TUNDRA = 209; + private static final int ID_DEEP_CANYON = 210; + private static final int ID_OCEAN_TRENCH = 211; + + public static CCBiomeGenBase DEEP_OCEAN; + public static CCBiomeGenBase OCEAN; + public static CCBiomeGenBase BEACH; + public static CCBiomeGenBase PLAINS; + public static CCBiomeGenBase ROLLING_HILLS; + public static CCBiomeGenBase FOREST; + public static CCBiomeGenBase LARGE_HILLS; + public static CCBiomeGenBase MOUNTAINS; + public static CCBiomeGenBase DESERT; + public static CCBiomeGenBase TUNDRA; + public static CCBiomeGenBase DEEP_CANYON; + public static CCBiomeGenBase OCEAN_TRENCH; + + private CCBiomes() {} + + /** Create and register all default biomes. Must be called during FML preInit. */ + public static void init() { + // Climate points: [temperature, humidity, continentalness, erosion] + DEEP_OCEAN = make(ID_DEEP_OCEAN, "Deep Ocean", new float[] { -0.2f, 0.5f, -0.9f, 0.5f }, 0, 15); + DEEP_OCEAN.ccTopBlock = new BlockMeta(Blocks.sand, 0); + DEEP_OCEAN.ccFillerBlock = new BlockMeta(Blocks.sand, 0); + + OCEAN = make(ID_OCEAN, "Ocean", new float[] { 0.0f, 0.5f, -0.6f, 0.4f }, 30, 15); + OCEAN.ccTopBlock = new BlockMeta(Blocks.sand, 0); + OCEAN.ccFillerBlock = new BlockMeta(Blocks.sand, 0); + + BEACH = make(ID_BEACH, "Beach", new float[] { 0.3f, 0.3f, -0.1f, 0.7f }, 63, 2); + BEACH.ccTopBlock = new BlockMeta(Blocks.sand, 0); + BEACH.ccFillerBlock = new BlockMeta(Blocks.sand, 0); + + PLAINS = make(ID_PLAINS, "Plains", new float[] { 0.4f, 0.2f, 0.3f, 0.8f }, 67, 4); + PLAINS.allowVillage = true; + + ROLLING_HILLS = make(ID_ROLLING_HILLS, "Rolling Hills", new float[] { 0.3f, 0.4f, 0.4f, 0.4f }, 67, 2); + + FOREST = make(ID_FOREST, "Forest", new float[] { 0.1f, 0.6f, 0.5f, 0.5f }, 67, 2); + + LARGE_HILLS = make(ID_LARGE_HILLS, "Large Hills", new float[] { 0.1f, 0.3f, 0.6f, 0.2f }, 72, 7); + + MOUNTAINS = make(ID_MOUNTAINS, "Mountains", new float[] { -0.1f, 0.2f, 0.7f, -0.5f }, 120, 30); + MOUNTAINS.ccTopBlock = new BlockMeta(Blocks.stone, 0); + MOUNTAINS.ccFillerBlock = new BlockMeta(Blocks.stone, 0); + MOUNTAINS.ccStoneBlock = new BlockMeta(Blocks.stone, 0); + MOUNTAINS.perturbationScale = 2.0f; + + DESERT = make(ID_DESERT, "Desert", new float[] { 0.9f, -0.7f, 0.2f, 0.6f }, 65, 4); + DESERT.ccTopBlock = new BlockMeta(Blocks.sand, 0); + DESERT.ccFillerBlock = new BlockMeta(Blocks.sand, 0); + DESERT.allowVillage = true; + + TUNDRA = make(ID_TUNDRA, "Tundra", new float[] { -0.8f, -0.2f, 0.3f, 0.5f }, 65, 1); + + DEEP_CANYON = make(ID_DEEP_CANYON, "Deep Canyon", new float[] { 0.5f, -0.4f, 0.5f, -0.8f }, -30, 5); + DEEP_CANYON.ccTopBlock = new BlockMeta(Blocks.stone, 0); + DEEP_CANYON.ccFillerBlock = new BlockMeta(Blocks.stone, 0); + DEEP_CANYON.ccStoneBlock = new BlockMeta(Blocks.stone, 0); + DEEP_CANYON.allowCanyons = true; + + OCEAN_TRENCH = make(ID_OCEAN_TRENCH, "Ocean Trench", new float[] { -0.3f, 0.5f, -0.8f, -0.9f }, -30, 5); + OCEAN_TRENCH.ccTopBlock = new BlockMeta(Blocks.sand, 0); + OCEAN_TRENCH.ccFillerBlock = new BlockMeta(Blocks.sand, 0); + OCEAN_TRENCH.allowTrenches = true; + } + + private static CCBiomeGenBase make(int id, String name, float[] climate, float rootHeight, float heightVariation) { + CCBiomeGenBase b = new CCBiomeGenBase(id, name, climate) { + + @SideOnly(Side.CLIENT) + @Override + public int getBiomeGrassColor(int p_150558_1_, int p_150558_2_, int p_150558_3_) { + return color; + } + }; + b.rootHeight = rootHeight; + b.heightVariation = heightVariation; + CCBiomeRegistry.register(b); + b.color = new HSVColor(climate[0] * 0.5f + 0.5f, climate[1] * 0.5f + 0.5f, climate[2] * 0.5f + 0.5f) + .toIntRGBA(); + return b; + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/climate/ClimateAxis.java b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/climate/ClimateAxis.java new file mode 100644 index 00000000..911a937a --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/climate/ClimateAxis.java @@ -0,0 +1,17 @@ +package com.cardinalstar.cubicchunks.worldgen.ccenhanced.climate; + +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelBuilder; + +/** + * A single independently-sampled noise axis used to locate a position in climate space. + * Implementations should return values remapped to approximately [-1, 1]. + */ +public interface ClimateAxis { + + String getName(); + + /** Sample the axis at world coordinates (x, z). Returns a value in [-1, 1]. */ + double sample(double x, double z); + + void compileShader(KernelBuilder builder, String functionName); +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/climate/ClimatePoint.java b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/climate/ClimatePoint.java new file mode 100644 index 00000000..00e80cf2 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/climate/ClimatePoint.java @@ -0,0 +1,22 @@ +package com.cardinalstar.cubicchunks.worldgen.ccenhanced.climate; + +/** + * An N-dimensional point in climate space, one float per axis. + * Axis order matches ClimateSystem.TEMPERATURE / HUMIDITY / CONTINENTALNESS / EROSION constants. + */ +public class ClimatePoint { + + public final float[] values; + + public ClimatePoint(float[] values) { + this.values = values; + } + + /** + * Returns the value for the given axis index, or 0 if the axis index is out of range + * (backward-compatible when new axes are added). + */ + public float get(int axis) { + return axis < values.length ? values[axis] : 0.0f; + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/climate/ClimateSystem.java b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/climate/ClimateSystem.java new file mode 100644 index 00000000..376b6727 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/climate/ClimateSystem.java @@ -0,0 +1,153 @@ +package com.cardinalstar.cubicchunks.worldgen.ccenhanced.climate; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import net.minecraft.world.ChunkCoordIntPair; + +import org.jetbrains.annotations.Nullable; + +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelBuilder; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelExecutor; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.StandardKernelExecutor; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferDataType; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferLayout; +import com.google.common.collect.ImmutableMap; +import com.gtnewhorizon.gtnhlib.hash.Fnv1a64; + +/** + * Holds the ordered list of climate axes, applies per-axis domain warping, and produces ClimatePoints for (x, z) world + * coordinates. + * + *

+ * Axis indices (0=temperature, 1=humidity, 2=continentalness, 3=erosion). Adding a fifth axis in a future update + * requires only appending to the axis list and extending the offset arrays. + */ +public class ClimateSystem { + + // Standard axis indices + public static final int TEMPERATURE = 0; + public static final int HUMIDITY = 1; + public static final int CONTINENTALNESS = 2; + public static final int EROSION = 3; + + // Domain warp parameters + private static final double WARP_FREQ = 1.0 / 800.0; + private static final double WARP_MAG = 200.0; + // Per-axis offsets — large irrational values to decorrelate samples from the same noise field. + private static final double[] WARP_OFFSET_X = { 0.0, 3141.5, 2718.2, 1414.2 }; + private static final double[] WARP_OFFSET_Z = { 1000.0, 4142.1, 1618.0, 2236.0 }; + + private final List axes; + private final ClimateAxis warp; + + public ClimateSystem(long worldSeed) { + axes = new ArrayList<>(); + // Axis 0: temperature — continental scale + addAxis(worldSeed, TEMPERATURE, "temperature", 4, 1.0 / 4000.0); + // Axis 1: humidity — regional scale + addAxis(worldSeed, HUMIDITY, "humidity", 4, 1.0 / 1500.0); + // Axis 2: continentalness — very slow; controls ocean vs. land masses + addAxis(worldSeed, CONTINENTALNESS, "continentalness", 3, 1.0 / 5000.0); + // Axis 3: erosion — regional flatness + addAxis(worldSeed, EROSION, "erosion", 4, 1.0 / 2000.0); + + // Warp noise: 2 octaves, 1/800 frequency. Use axisIndex=-1 as seed slot. + long warpSeed = Fnv1a64.hashStep(Fnv1a64.hashStep(Fnv1a64.initialState(), worldSeed), -1L); + this.warp = new NoiseClimateAxis("warp", warpSeed, 2, WARP_FREQ); + } + + private void addAxis(long worldSeed, int axisIndex, String name, int octaves, double frequency) { + long seed = Fnv1a64.hashStep(Fnv1a64.hashStep(Fnv1a64.initialState(), worldSeed), (long) axisIndex); + axes.add(new NoiseClimateAxis(name, seed, octaves, frequency)); + } + + /** + * Sample the climate at world coordinates (x, z). Domain warping is applied to each axis independently using + * per-axis coordinate offsets. + */ + public float[] sample(double x, double z, float @Nullable [] pooled) { + if (pooled == null) pooled = new float[axes.size()]; + + for (int i = 0; i < axes.size(); i++) { + double ox = WARP_OFFSET_X[i]; + double oz = WARP_OFFSET_Z[i]; + // Sample warp displacement for this axis + double wx = warp.sample(x + ox, z + oz); + double wz = warp.sample(x + ox + 500.0, z + oz + 500.0); + pooled[i] = (float) axes.get(i) + .sample(x + wx * WARP_MAG, z + wz * WARP_MAG); + } + + return pooled; + } + + /** + * Creates a kernel that samples one climate axis for a chunk, writing into the axis-major noise buffer at the slice + * for {@code axisIndex}. The first axis (0) allocates the output buffer; subsequent axes receive it as input + * {@code "noise"} and write their slice into the same buffer, returning it unchanged. + */ + public KernelExecutor createAxisKernel(int axisIndex) { + return new AxisSamplerKernelExecutor(axisIndex, axes.get(axisIndex)); + } + + private class AxisSamplerKernelExecutor extends StandardKernelExecutor { + + private final int axisIndex; + private final ClimateAxis axis; + + public AxisSamplerKernelExecutor(int axisIndex, ClimateAxis axis) { + this.axisIndex = axisIndex; + this.axis = axis; + } + + @Override + protected String generateKernel(KernelBuilder builder) { + builder.addParameter(BufferDataType.i32, "offsetX"); + builder.addParameter(BufferDataType.i32, "offsetZ"); + builder.addOutputBuffer("noise", new BufferLayout(BufferDataType.f32, 16, 16)); + + ClimateSystem.this.warp.compileShader(builder, "warp"); + axis.compileShader(builder, axis.getName()); + + builder.addMacro("WARP_OFFSET_X", (float) WARP_OFFSET_X[axisIndex]); + builder.addMacro("WARP_OFFSET_Z", (float) WARP_OFFSET_Z[axisIndex]); + builder.addMacro("AXIS_OUTPUT_OFFSET", axisIndex * 256); + + return """ + #version 460 + + layout(local_size_x = 16, local_size_y = 16) in; + + layout(set = 0, binding = 0) readonly buffer Constants { uint constants[]; }; + layout(set = 1, binding = 0) buffer Arena { uint arena[]; }; + + $pc + + $preamble + + void main() { + uint x = gl_GlobalInvocationID.x; + uint z = gl_GlobalInvocationID.y; + + int gx = GET_OFFSET_X + int(x); + int gz = GET_OFFSET_Z + int(z); + + $logic + + float warpX = warp(gx + WARP_OFFSET_X, gz + WARP_OFFSET_Z) * 200.0f; + float warpZ = warp(gx + WARP_OFFSET_X + 500.0f, gz + WARP_OFFSET_Z + 500.0f) * 200.0f; + + SET_NOISE(AXIS_OUTPUT_OFFSET + ((z << 4) | x), $axisFunc(gx + warpX, gz + warpZ)); + } + """.replace("$logic", builder.logic.toString()) + .replace("$axisFunc", axis.getName()); + } + + @Override + protected Map getParameters(ChunkCoordIntPair key) { + return ImmutableMap.of("offsetX", key.chunkXPos << 4, "offsetZ", key.chunkZPos << 4); + } + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/climate/NoiseClimateAxis.java b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/climate/NoiseClimateAxis.java new file mode 100644 index 00000000..4d40fc30 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/climate/NoiseClimateAxis.java @@ -0,0 +1,64 @@ +package com.cardinalstar.cubicchunks.worldgen.ccenhanced.climate; + +import java.util.Random; + +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelBuilder; +import com.cardinalstar.cubicchunks.world.worldgen.noise.NoiseSampler; +import com.cardinalstar.cubicchunks.world.worldgen.noise.NormalizedSampler; +import com.cardinalstar.cubicchunks.world.worldgen.noise.OctavesSampler; +import com.cardinalstar.cubicchunks.world.worldgen.noise.ScaledSampler; + +/** + * ClimateAxis implementation backed by a ScaledNoise(OctavesSampler). + * + *

+ * Raw FBM output (approximately Gaussian) is normalised to [-1, 1] and then remapped + * through a precomputed LUT to approximate a uniform distribution. This prevents + * centre-biased noise from over-representing middle-range biomes in Voronoi lookup. + */ +public class NoiseClimateAxis implements ClimateAxis { + + private final String name; + private final NoiseSampler noise; + + /** + * @param name Axis name (e.g. "temperature") + * @param seed RNG seed for the underlying SimplexNoiseSampler octaves + * @param octaves Number of FBM octaves + * @param frequency Base sampling frequency in 1/blocks (e.g. 1/4000) + */ + public NoiseClimateAxis(String name, long seed, int octaves, double frequency) { + this.name = name; + this.noise = new NormalizedSampler(new ScaledSampler(new OctavesSampler(new Random(seed), octaves), frequency)); + } + + @Override + public String getName() { + return name; + } + + @Override + public double sample(double x, double z) { + return noise.sample(x, z); + } + + @Override + public void compileShader(KernelBuilder builder, String functionName) { + String existingLogic = builder.logic.toString(); + + String result = this.noise.compileKernel2D(builder, "x", "y"); + + String function = """ + float $name(float x, float y) { + $logic + return $result; + } + """.replace("$name", functionName) + .replace("$logic", builder.logic.toString()) + .replace("$result", result); + + builder.preamble.append(function); + builder.logic.setLength(0); + builder.logic.append(existingLogic); + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/surface/CCSurfacePainter.java b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/surface/CCSurfacePainter.java new file mode 100644 index 00000000..bc1e3ae3 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/surface/CCSurfacePainter.java @@ -0,0 +1,100 @@ +package com.cardinalstar.cubicchunks.worldgen.ccenhanced.surface; + +import javax.annotation.ParametersAreNonnullByDefault; + +import net.minecraft.init.Blocks; +import net.minecraft.world.World; +import net.minecraft.world.chunk.storage.ExtendedBlockStorage; + +import com.cardinalstar.cubicchunks.worldgen.ccenhanced.biome.BiomeLookupResult; +import com.cardinalstar.cubicchunks.worldgen.ccenhanced.biome.CCBiomeGenBase; +import com.cardinalstar.cubicchunks.worldgen.ccenhanced.terrain.ColumnContext; +import com.gtnewhorizon.gtnhlib.util.data.ImmutableBlockMeta; + +/** + * Paints biome-appropriate surface blocks (top, filler) onto terrain generated by + * CCTerrainGenerator, and fills ocean areas below sea level with still water. + * + *

+ * Operates on a 16-entry EBS array (index = cubeY, range 0–15). Missing entries are + * created on demand (e.g. for water that sits above an air-region ocean floor). + */ +@ParametersAreNonnullByDefault +public class CCSurfacePainter { + + private static final int SEA_LEVEL = 64; + + /** + * Paints surface blocks and fills water into the EBS array for a single chunk column. + * Modifies {@code ebsArray} in-place; entries may be created for water-fill cubes. + * + * @param world world instance (for hasNoSky when allocating EBS) + * @param ebsArray 16-entry array indexed by cubeY (0–15); entries may be null + * @param ctx column context from CCTerrainGenerator + * @param grid 5×5 biome grid from CCBiomeCache for this chunk + */ + public void paint(World world, ExtendedBlockStorage[] ebsArray, ColumnContext ctx, BiomeLookupResult[] grid) { + for (int bx = 0; bx < 16; bx++) { + for (int bz = 0; bz < 16; bz++) { + int sy = ctx.getSurfaceY(bx, bz); + CCBiomeGenBase biome = grid[bz << 4 | bx].primary; + + paintSurface(world, ebsArray, bx, bz, sy, biome); + fillWater(world, ebsArray, bx, bz, sy); + } + } + } + + // ------------------------------------------------------------------------- + // Internals + // ------------------------------------------------------------------------- + + /** Places ccTopBlock at surfaceY-1 and ccFillerBlock for the next fillerDepth blocks below. */ + private void paintSurface(World world, ExtendedBlockStorage[] ebsArray, int bx, int bz, int sy, + CCBiomeGenBase biome) { + int topY = sy - 1; + if (topY < 0 || topY >= 256) return; + + setBlock(world, ebsArray, bx, topY, bz, biome.ccTopBlock); + + for (int i = 1; i <= biome.fillerDepth; i++) { + int y = topY - i; + if (y < 0) break; + setBlock(world, ebsArray, bx, y, bz, biome.ccFillerBlock); + } + } + + /** Fills still water from surfaceY up to SEA_LEVEL-1 for below-sea columns. */ + private void fillWater(World world, ExtendedBlockStorage[] ebsArray, int bx, int bz, int sy) { + if (sy >= SEA_LEVEL) return; + int waterBottom = Math.max(0, sy); + for (int y = waterBottom; y < SEA_LEVEL; y++) { + int cubeY = y >> 4; + int localY = y & 0xF; + ExtendedBlockStorage ebs = getOrCreate(world, ebsArray, cubeY); + ebs.func_150818_a(bx, localY, bz, Blocks.water); + } + } + + /** Sets a block+meta at world-Y y, allocating EBS if needed. */ + private void setBlock(World world, ExtendedBlockStorage[] ebsArray, int bx, int y, int bz, ImmutableBlockMeta bm) { + int cubeY = y >> 4; + int localY = y & 0xF; + if (cubeY < 0 || cubeY >= ebsArray.length) return; + + ExtendedBlockStorage ebs = getOrCreate(world, ebsArray, cubeY); + ebs.func_150818_a(bx, localY, bz, bm.getBlock()); + int meta = bm.getBlockMeta(); + if (meta != 0) { + ebs.setExtBlockMetadata(bx, localY, bz, meta); + } + } + + /** Returns the EBS at cubeY, creating an empty one if currently null. */ + private ExtendedBlockStorage getOrCreate(World world, ExtendedBlockStorage[] ebsArray, int cubeY) { + if (ebsArray[cubeY] == null) { + ebsArray[cubeY] = new ExtendedBlockStorage(cubeY * 16, !world.provider.hasNoSky); + } + return ebsArray[cubeY]; + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/terrain/CCBiomeCache.java b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/terrain/CCBiomeCache.java new file mode 100644 index 00000000..7f40d41a --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/terrain/CCBiomeCache.java @@ -0,0 +1,75 @@ +package com.cardinalstar.cubicchunks.worldgen.ccenhanced.terrain; + +import com.cardinalstar.cubicchunks.util.Coords; +import com.cardinalstar.cubicchunks.worldgen.ccenhanced.biome.BiomeLookupResult; +import com.cardinalstar.cubicchunks.worldgen.ccenhanced.biome.CCBiomeRegistry; +import com.cardinalstar.cubicchunks.worldgen.ccenhanced.climate.ClimateSystem; + +import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; + +/** + * World-level LRU cache mapping (chunkX, chunkZ) to a 5×5 biome lookup grid. + * + *

+ * The 16×16 grid provides one biome lookup per block column within the chunk. + * + *

+ * During cube generation, the per-column biome blend weights are looked up directly + * by block position within the chunk. + * + *

+ * Also used by canyon/trench edge blending to query neighbor-chunk biomes without + * requiring those chunks to be loaded. + */ +public class CCBiomeCache { + + private static final int CACHE_CAPACITY = 512; + + private final ClimateSystem climate; + + // Access-order LinkedHashMap used as an LRU cache. Key: chunkX<<32|chunkZ (unsigned). + private final Long2ObjectLinkedOpenHashMap cache = new Long2ObjectLinkedOpenHashMap<>( + CACHE_CAPACITY, + 0.75f); + + public CCBiomeCache(ClimateSystem climate) { + this.climate = climate; + } + + /** + * Returns the cached 5×5 BiomeLookupResult grid for the given chunk, computing it on demand. + * Array is indexed as {@code grid[gx + gz * GRID_SIZE]} where gx, gz ∈ [0, 4]. + */ + public BiomeLookupResult[] getGrid(int chunkX, int chunkZ) { + long key = Coords.packChunk(chunkX, chunkZ); + BiomeLookupResult[] grid = cache.getAndMoveToFirst(key); + + if (grid == null) { + grid = computeGrid(chunkX, chunkZ); + cache.putAndMoveToFirst(key, grid); + + while (cache.size() > CACHE_CAPACITY) cache.removeLast(); + } + + return grid; + } + + private BiomeLookupResult[] computeGrid(int chunkX, int chunkZ) { + BiomeLookupResult[] grid = new BiomeLookupResult[16 * 16]; + int originX = chunkX << 4; + int originZ = chunkZ << 4; + + float[] climatePoint = null; + + for (int localX = 0; localX < 16; localX++) { + for (int localZ = 0; localZ < 16; localZ++) { + int bx = originX + localX; + int bz = originZ + localZ; + climatePoint = climate.sample(bx, bz, climatePoint); + grid[localZ << 4 | localX] = CCBiomeRegistry.lookup(climatePoint, 3); + } + } + + return grid; + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/terrain/CCTerrainGenerator.java b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/terrain/CCTerrainGenerator.java new file mode 100644 index 00000000..f521c2f6 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/terrain/CCTerrainGenerator.java @@ -0,0 +1,150 @@ +package com.cardinalstar.cubicchunks.worldgen.ccenhanced.terrain; + +import java.util.Random; + +import javax.annotation.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; + +import net.minecraft.init.Blocks; +import net.minecraft.world.World; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.chunk.storage.ExtendedBlockStorage; + +import com.cardinalstar.cubicchunks.world.worldgen.noise.NoiseSampler; +import com.cardinalstar.cubicchunks.world.worldgen.noise.OctavesSampler; +import com.cardinalstar.cubicchunks.world.worldgen.noise.ScaledSampler; +import com.cardinalstar.cubicchunks.worldgen.ccenhanced.biome.BiomeLookupResult; +import com.gtnewhorizon.gtnhlib.hash.Fnv1a64; + +/** + * Computes the terrain heightmap and fills ExtendedBlockStorage sections. + * + *

+ * Step 3: pure heightmap terrain, all solid blocks are stone. Surface painting + * (grass/dirt/sand) and 3D perturbation are added in subsequent steps. + * + *

+ * Heightmap formula: + * + *

+ * surfaceY = blendedRoot + blendedVar * hvNoise
+ * 
+ * + * rootHeight and heightVariation are in world-Y block coordinates; no additional scaling is applied. + * surfaceY is "first air Y": block at y < surfaceY is solid, block at y >= surfaceY is air. + */ +@ParametersAreNonnullByDefault +public class CCTerrainGenerator { + + /** HV noise seed slot — distinct from the climate axis slots (0–3) and warp slot (-1). */ + private static final long HV_SEED_SLOT = 100L; + + private final NoiseSampler hvNoise; + + public CCTerrainGenerator(long worldSeed) { + long hvSeed = Fnv1a64.hashStep(Fnv1a64.hashStep(Fnv1a64.initialState(), worldSeed), HV_SEED_SLOT); + // 4 octaves; 1/400 base frequency → hills ~400 blocks wide + hvNoise = new ScaledSampler(new OctavesSampler(new Random(hvSeed), 4), 1.0 / 400.0); + } + + // ------------------------------------------------------------------------- + // Column context + // ------------------------------------------------------------------------- + + /** + * Computes surfaceY for all 256 block columns in the chunk, writes to chunk.heightMap, + * and returns a ColumnContext holding the values plus min/max for fast-path cube skipping. + */ + public ColumnContext computeColumnContext(Chunk chunk, int chunkX, int chunkZ, CCBiomeCache biomeCache) { + + BiomeLookupResult[] grid = biomeCache.getGrid(chunkX, chunkZ); + + float[] gridRoot = new float[16 * 16]; + float[] gridVar = new float[16 * 16]; + + for (int bx = 0; bx < 16; bx++) { + for (int bz = 0; bz < 16; bz++) { + int idx = bz << 4 | bx; + BiomeLookupResult r = grid[idx]; + gridRoot[idx] = r.blend(b -> b.rootHeight); + gridVar[idx] = r.blend(b -> b.heightVariation); + } + } + + int originX = chunkX * 16; + int originZ = chunkZ * 16; + + int[] surfaceY = new int[16 * 16]; + int minY = Integer.MAX_VALUE; + int maxY = Integer.MIN_VALUE; + + for (int bx = 0; bx < 16; bx++) { + for (int bz = 0; bz < 16; bz++) { + int idx = bz << 4 | bx; + + double hv = hvNoise.sample(originX + bx, originZ + bz); + + int sy = (int) (gridRoot[idx] + gridVar[idx] * (float) hv); + surfaceY[bx + bz * 16] = sy; + + // Write to vanilla heightmap. Convention: store surfaceY (first-air-Y). + chunk.heightMap[bz << 4 | bx] = sy; + + if (sy < minY) minY = sy; + if (sy > maxY) maxY = sy; + } + } + + return new ColumnContext(surfaceY, minY, maxY); + } + + // ------------------------------------------------------------------------- + // Cube filling + // ------------------------------------------------------------------------- + + /** + * Builds an ExtendedBlockStorage for the given cube, filling solid positions with stone. + * Returns null for fully-air cubes (optimization: avoids allocating empty EBS objects). + * + *

+ * Solid rule: block at world-Y y is solid iff y < surfaceY. + */ + @Nullable + public ExtendedBlockStorage buildEbs(World world, ColumnContext ctx, int cubeY) { + int cubeMinY = cubeY * 16; + int cubeMaxY = cubeMinY + 15; + + // Fast path: cube entirely above the highest surface → all air + if (cubeMinY >= ctx.maxSurfaceY) return null; + + int yBase = cubeMinY; + boolean storeSkylight = !world.provider.hasNoSky; + ExtendedBlockStorage ebs = new ExtendedBlockStorage(yBase, storeSkylight); + + // Fast path: cube entirely below the lowest surface → all stone + if (cubeMaxY < ctx.minSurfaceY) { + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + for (int y = 0; y < 16; y++) { + ebs.func_150818_a(x, y, z, Blocks.stone); + } + } + } + return ebs; + } + + // General case: per-column density check + for (int bx = 0; bx < 16; bx++) { + for (int bz = 0; bz < 16; bz++) { + int sy = ctx.getSurfaceY(bx, bz); + // Stone for all y in [cubeMinY, min(cubeMaxY, sy-1)] + int top = Math.min(cubeMaxY, sy - 1); + for (int y = cubeMinY; y <= top; y++) { + ebs.func_150818_a(bx, y - cubeMinY, bz, Blocks.stone); + } + } + } + + return ebs; + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/terrain/ColumnContext.java b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/terrain/ColumnContext.java new file mode 100644 index 00000000..3632ce44 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/terrain/ColumnContext.java @@ -0,0 +1,34 @@ +package com.cardinalstar.cubicchunks.worldgen.ccenhanced.terrain; + +/** + * Per-column data computed once during provideColumn and reused across all provideCube calls + * for the same column. + * + *

+ * surfaceY is stored here (mirrored in chunk.heightMap for structure access). + * Canyon and trench noise arrays will be added in Step 6. + */ +public class ColumnContext { + + /** + * SurfaceY for each block column, indexed as [bx + bz * 16]. Values are "first air Y" + * (i.e. the block at y = surfaceY is air; solid terrain exists at y < surfaceY). + */ + public final int[] surfaceY; + + /** Minimum surfaceY across the 16×16 column. Fast-path: cubes fully below this are all stone. */ + public final int minSurfaceY; + + /** Maximum surfaceY across the 16×16 column. Fast-path: cubes fully above this are all air. */ + public final int maxSurfaceY; + + public ColumnContext(int[] surfaceY, int minSurfaceY, int maxSurfaceY) { + this.surfaceY = surfaceY; + this.minSurfaceY = minSurfaceY; + this.maxSurfaceY = maxSurfaceY; + } + + public int getSurfaceY(int bx, int bz) { + return surfaceY[bx + bz * 16]; + } +}