From fbb84bc09d1a237a357b4918ee9a79569e08d161 Mon Sep 17 00:00:00 2001 From: RecursivePineapple Date: Sun, 31 May 2026 10:37:11 -0400 Subject: [PATCH 1/8] Angelica Compat + Cleanup --- .../api/compat/CubicChunksVideoSettings.java | 4 +-- .../event/handlers/ClientEventHandler.java | 4 +-- .../cubicchunks/mixin/Mixins.java | 6 ---- .../common/MixinExtendedBlockStorage.java | 29 ------------------- .../modcompat/angelica/AngelicaInterop.java | 18 ------------ .../modcompat/angelica/IAngelicaDelegate.java | 13 --------- .../network/PacketEncoderColumn.java | 6 ---- .../network/PacketEncoderCube.java | 6 ---- .../network/PacketEncoderUnloadColumn.java | 6 ---- .../network/PacketEncoderUnloadCube.java | 6 ---- 10 files changed, 4 insertions(+), 94 deletions(-) delete mode 100644 src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/MixinExtendedBlockStorage.java delete mode 100644 src/main/java/com/cardinalstar/cubicchunks/modcompat/angelica/AngelicaInterop.java delete mode 100644 src/main/java/com/cardinalstar/cubicchunks/modcompat/angelica/IAngelicaDelegate.java 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/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/mixin/Mixins.java b/src/main/java/com/cardinalstar/cubicchunks/mixin/Mixins.java index b199a590..1a601ab3 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) 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/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/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..ea4677c9 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/network/PacketEncoderCube.java +++ b/src/main/java/com/cardinalstar/cubicchunks/network/PacketEncoderCube.java @@ -31,7 +31,6 @@ 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; @@ -125,11 +124,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()); - } } } From 4097a64905e435450931d80e078cb22168660fdc Mon Sep 17 00:00:00 2001 From: RecursivePineapple Date: Tue, 2 Jun 2026 13:24:47 -0400 Subject: [PATCH 2/8] Add Celeritas PositionUtil coord packing mixins --- dependencies.gradle | 3 +- .../cubicchunks/mixin/Mixins.java | 8 ++- .../mixin/early/mod/MixinPositionUtil.java | 56 +++++++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/cardinalstar/cubicchunks/mixin/early/mod/MixinPositionUtil.java 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/main/java/com/cardinalstar/cubicchunks/mixin/Mixins.java b/src/main/java/com/cardinalstar/cubicchunks/mixin/Mixins.java index 1a601ab3..7a31c4af 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/mixin/Mixins.java +++ b/src/main/java/com/cardinalstar/cubicchunks/mixin/Mixins.java @@ -305,7 +305,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/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); + } +} From d7340febfd41bf98362130a19959609107dc2a57 Mon Sep 17 00:00:00 2001 From: RecursivePineapple Date: Tue, 2 Jun 2026 13:25:42 -0400 Subject: [PATCH 3/8] Add explicit dep on gtnhlib --- src/main/java/com/cardinalstar/cubicchunks/CubicChunks.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/cardinalstar/cubicchunks/CubicChunks.java b/src/main/java/com/cardinalstar/cubicchunks/CubicChunks.java index 96d4c517..706f6d87 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/CubicChunks.java +++ b/src/main/java/com/cardinalstar/cubicchunks/CubicChunks.java @@ -74,7 +74,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; From c9b3d5d5754919127e2a9920df2f2effcba78e09 Mon Sep 17 00:00:00 2001 From: RecursivePineapple Date: Tue, 2 Jun 2026 13:42:55 -0400 Subject: [PATCH 4/8] initial work on new world type --- .../cardinalstar/cubicchunks/CubicChunks.java | 2 + .../ccenhanced/CCEnhancedWorldGenerator.java | 170 ++++++++++++++++++ .../ccenhanced/CCEnhancedWorldType.java | 42 +++++ 3 files changed, 214 insertions(+) create mode 100644 src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/CCEnhancedWorldGenerator.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/CCEnhancedWorldType.java diff --git a/src/main/java/com/cardinalstar/cubicchunks/CubicChunks.java b/src/main/java/com/cardinalstar/cubicchunks/CubicChunks.java index 706f6d87..b96218e1 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/CubicChunks.java +++ b/src/main/java/com/cardinalstar/cubicchunks/CubicChunks.java @@ -52,6 +52,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; @@ -121,6 +122,7 @@ public void preInit(FMLPreInitializationEvent e) { registerAnvil3dStorageFormatProvider(); VanillaCubicWorldType.init(); + CCEnhancedWorldType.init(); LOGGER.debug("Registered world types"); 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..f0539fde --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/CCEnhancedWorldGenerator.java @@ -0,0 +1,170 @@ +package com.cardinalstar.cubicchunks.worldgen.ccenhanced; + +import java.util.ArrayList; +import java.util.List; + +import javax.annotation.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; + +import net.minecraft.entity.EnumCreatureType; +import net.minecraft.init.Blocks; +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 com.cardinalstar.cubicchunks.api.ICube; +import com.cardinalstar.cubicchunks.api.worldgen.GenerationResult; +import com.cardinalstar.cubicchunks.api.worldgen.IWorldGenerator; +import com.cardinalstar.cubicchunks.world.core.IColumnInternal; +import com.cardinalstar.cubicchunks.world.cube.Cube; + +@ParametersAreNonnullByDefault +public class CCEnhancedWorldGenerator implements IWorldGenerator { + + private static final int SEA_LEVEL = 64; + + // Flat world layout: + // cubeY 0-2 (blocks 0-47): all stone + // cubeY 3 (blocks 48-63): stone, top layer (local y=15) = dirt + // cubeY 4 (blocks 64-79): grass at local y=0, air above + // cubeY 5+ (blocks 80+ ): air + private static final int SURFACE_CUBE_Y = SEA_LEVEL / 16; // 4 + private static final int DIRT_CUBE_Y = SURFACE_CUBE_Y - 1; // 3 + + private final World world; + + public CCEnhancedWorldGenerator(World world) { + this.world = world; + } + + // ------------------------------------------------------------------------- + // Column + // ------------------------------------------------------------------------- + + @Override + public GenerationResult provideColumn(World world, int columnX, int columnZ) { + Chunk chunk = new Chunk(world, columnX, columnZ); + ((IColumnInternal) chunk).setColumn(true); + + List cubes = new ArrayList<>(16); + for (int cy = 0; cy < 16; cy++) { + cubes.add(new Cube(chunk, cy, createFlatEbs(cy))); + } + + 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 if 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); + } + for (int cy = 0; cy < 16; cy++) { + generatedCubes.add(new Cube(chunk, cy, createFlatEbs(cy))); + } + } + + // Cubes outside [0,16) are always air. + if (cubeY < 0 || cubeY >= 16) { + generatedCubes.add(new Cube(chunk, cubeY, (ExtendedBlockStorage) null)); + } + + // 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 + // ------------------------------------------------------------------------- + + /** + * Build an ExtendedBlockStorage for the flat world at the given cube Y. + * Returns null for fully-air cubes (cubeY outside the filled range). + */ + @Nullable + private ExtendedBlockStorage createFlatEbs(int cubeY) { + if (cubeY < 0 || cubeY > SURFACE_CUBE_Y) { + return null; // air + } + + int yBase = cubeY * 16; + boolean storeSkylight = !world.provider.hasNoSky; + ExtendedBlockStorage ebs = new ExtendedBlockStorage(yBase, storeSkylight); + + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + if (cubeY < DIRT_CUBE_Y) { + // All stone + for (int ly = 0; ly < 16; ly++) { + ebs.func_150818_a(x, ly, z, Blocks.stone); + } + } else if (cubeY == DIRT_CUBE_Y) { + // Stone with dirt cap + for (int ly = 0; ly < 15; ly++) { + ebs.func_150818_a(x, ly, z, Blocks.stone); + } + ebs.func_150818_a(x, 15, z, Blocks.dirt); + } else { + // cubeY == SURFACE_CUBE_Y: grass at local y=0, air above + ebs.func_150818_a(x, 0, z, Blocks.grass); + } + } + } + + return ebs; + } +} 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..7e031773 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/CCEnhancedWorldType.java @@ -0,0 +1,42 @@ +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; + +@ParametersAreNonnullByDefault +public class CCEnhancedWorldType extends WorldType implements ICubicWorldType { + + public static CCEnhancedWorldType INSTANCE; + + private CCEnhancedWorldType() { + super("Cubic Chunks"); + } + + public static void init() { + INSTANCE = new CCEnhancedWorldType(); + } + + @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; + } +} From 40dc6d32efb7feefa399bbee8e38fd95fba53fd7 Mon Sep 17 00:00:00 2001 From: RecursivePineapple Date: Tue, 2 Jun 2026 14:46:08 -0400 Subject: [PATCH 5/8] biomes --- .../ccenhanced/CCEnhancedWorldType.java | 2 + .../ccenhanced/biome/BiomeLookupResult.java | 39 ++++++ .../ccenhanced/biome/CCBiomeGenBase.java | 59 +++++++++ .../ccenhanced/biome/CCBiomeRegistry.java | 117 ++++++++++++++++++ .../ccenhanced/biome/defaults/CCBiomes.java | 105 ++++++++++++++++ .../ccenhanced/climate/ClimateAxis.java | 22 ++++ .../ccenhanced/climate/ClimatePoint.java | 22 ++++ .../ccenhanced/climate/ClimateSystem.java | 90 ++++++++++++++ .../ccenhanced/climate/NoiseClimateAxis.java | 71 +++++++++++ .../ccenhanced/terrain/CCBiomeCache.java | 83 +++++++++++++ 10 files changed, 610 insertions(+) create mode 100644 src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/BiomeLookupResult.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/CCBiomeGenBase.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/CCBiomeRegistry.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/defaults/CCBiomes.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/climate/ClimateAxis.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/climate/ClimatePoint.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/climate/ClimateSystem.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/climate/NoiseClimateAxis.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/terrain/CCBiomeCache.java diff --git a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/CCEnhancedWorldType.java b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/CCEnhancedWorldType.java index 7e031773..16b15a74 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/CCEnhancedWorldType.java +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/CCEnhancedWorldType.java @@ -11,6 +11,7 @@ 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 { @@ -23,6 +24,7 @@ private CCEnhancedWorldType() { public static void init() { INSTANCE = new CCEnhancedWorldType(); + CCBiomes.init(); } @Override 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..d644106f --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/CCBiomeRegistry.java @@ -0,0 +1,117 @@ +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 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[n]; + float[] best = new float[n]; + Arrays.fill(best, Float.MAX_VALUE); + + // Selection: keep the n smallest distances + for (int i = 0; i < dists.length; i++) { + // Find the slot in our top-n that holds the worst (largest) distance + int worstSlot = 0; + for (int j = 1; j < n; j++) { + if (best[j] > best[worstSlot]) worstSlot = j; + } + if (dists[i] < best[worstSlot]) { + best[worstSlot] = dists[i]; + indices[worstSlot] = i; + } + } + + // Insertion-sort the n results by distance (n is tiny — 3 by default) + for (int i = 1; i < n; i++) { + float key = best[i]; + int idx = indices[i]; + int j = i - 1; + while (j >= 0 && best[j] > key) { + best[j + 1] = best[j]; + indices[j + 1] = indices[j]; + j--; + } + best[j + 1] = key; + indices[j + 1] = idx; + } + + return indices; + } +} 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..1cc0e18d --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/defaults/CCBiomes.java @@ -0,0 +1,105 @@ +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.util.data.BlockMeta; + +/** + * 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 }, -1.8f, 0.1f); + 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 }, -1.0f, 0.1f); + 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 }, -0.1f, 0.05f); + 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 }, 0.1f, 0.05f); + PLAINS.allowVillage = true; + + ROLLING_HILLS = make(ID_ROLLING_HILLS, "Rolling Hills", new float[] { 0.3f, 0.4f, 0.4f, 0.4f }, 0.4f, 0.3f); + + FOREST = make(ID_FOREST, "Forest", new float[] { 0.1f, 0.6f, 0.5f, 0.5f }, 0.3f, 0.2f); + + LARGE_HILLS = make(ID_LARGE_HILLS, "Large Hills", new float[] { 0.1f, 0.3f, 0.6f, 0.2f }, 0.7f, 0.5f); + + MOUNTAINS = make(ID_MOUNTAINS, "Mountains", new float[] { -0.1f, 0.2f, 0.7f, -0.5f }, 1.0f, 0.8f); + 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 }, 0.1f, 0.1f); + 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 }, 0.1f, 0.1f); + + DEEP_CANYON = make(ID_DEEP_CANYON, "Deep Canyon", new float[] { 0.5f, -0.4f, 0.5f, -0.8f }, 0.2f, 0.1f); + 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 }, -2.5f, 0.2f); + 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); + b.rootHeight = rootHeight; + b.heightVariation = heightVariation; + CCBiomeRegistry.register(b); + 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..53b52f65 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/climate/ClimateAxis.java @@ -0,0 +1,22 @@ +package com.cardinalstar.cubicchunks.worldgen.ccenhanced.climate; + +/** + * 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); + + /** + * Default position on this axis for biomes that were registered before this axis existed + * or whose climate point is shorter than the current axis list. + * Most axes should return 0; some (e.g. a future "weirdness" axis) may prefer a different neutral. + */ + default float defaultValue() { + return 0.0f; + } +} 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..ba8c5273 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/climate/ClimateSystem.java @@ -0,0 +1,90 @@ +package com.cardinalstar.cubicchunks.worldgen.ccenhanced.climate; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +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.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 static final double REMAP_EXPONENT = 0.65; + + private final List axes; + private final NoiseSampler warpNoise; + + 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); + warpNoise = new ScaledNoise(new OctavesSampler(new Random(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, REMAP_EXPONENT)); + } + + /** + * Sample the climate at world coordinates (x, z). + * Domain warping is applied to each axis independently using per-axis coordinate offsets. + */ + public ClimatePoint sample(double x, double z) { + float[] values = new float[axes.size()]; + for (int i = 0; i < axes.size(); i++) { + double ox = warpOffsetX(i); + double oz = warpOffsetZ(i); + // Sample warp displacement for this axis + double wx = warpNoise.sample(x + ox, z + oz); + double wz = warpNoise.sample(x + ox + 500.0, z + oz + 500.0); + values[i] = (float) axes.get(i) + .sample(x + wx * WARP_MAG, z + wz * WARP_MAG); + } + return new ClimatePoint(values); + } + + public int numAxes() { + return axes.size(); + } + + private double warpOffsetX(int i) { + return i < WARP_OFFSET_X.length ? WARP_OFFSET_X[i] : i * 1000.0; + } + + private double warpOffsetZ(int i) { + return i < WARP_OFFSET_Z.length ? WARP_OFFSET_Z[i] : i * 1000.0 + 500.0; + } +} 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..5c1605d5 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/climate/NoiseClimateAxis.java @@ -0,0 +1,71 @@ +package com.cardinalstar.cubicchunks.worldgen.ccenhanced.climate; + +import java.util.Random; + +import com.cardinalstar.cubicchunks.world.worldgen.noise.NoiseSampler; +import com.cardinalstar.cubicchunks.world.worldgen.noise.OctavesSampler; +import com.cardinalstar.cubicchunks.world.worldgen.noise.ScaledNoise; + +/** + * 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 static final int LUT_SIZE = 1024; + + private final String name; + private final NoiseSampler noise; + private final float[] remapLut; + /** Sum of octave amplitudes; used to normalise FBM output to [-1, 1] before LUT lookup. */ + private final double amplitudeNorm; + + /** + * @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) + * @param remapExponent Power-curve exponent for distribution remapping; <1 spreads extremes + */ + public NoiseClimateAxis(String name, long seed, int octaves, double frequency, double remapExponent) { + this.name = name; + this.noise = new ScaledNoise(new OctavesSampler(new Random(seed), octaves), frequency); + this.remapLut = buildLut(remapExponent); + // Sum of amplitudes for n octaves: 2 * (1 - 0.5^n) + this.amplitudeNorm = 2.0 * (1.0 - Math.pow(0.5, octaves)); + } + + @Override + public String getName() { + return name; + } + + @Override + public double sample(double x, double z) { + double raw = noise.sample(x, z); + double normalized = raw / amplitudeNorm; + return remap(normalized); + } + + private float remap(double v) { + v = Math.max(-1.0, Math.min(1.0, v)); + 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 remapLut[lo] + frac * (remapLut[hi] - remapLut[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; + } +} 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..d35cf154 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/terrain/CCBiomeCache.java @@ -0,0 +1,83 @@ +package com.cardinalstar.cubicchunks.worldgen.ccenhanced.terrain; + +import java.util.LinkedHashMap; +import java.util.Map; + +import com.cardinalstar.cubicchunks.worldgen.ccenhanced.biome.BiomeLookupResult; +import com.cardinalstar.cubicchunks.worldgen.ccenhanced.biome.CCBiomeRegistry; +import com.cardinalstar.cubicchunks.worldgen.ccenhanced.climate.ClimatePoint; +import com.cardinalstar.cubicchunks.worldgen.ccenhanced.climate.ClimateSystem; + +/** + * World-level LRU cache mapping (chunkX, chunkZ) to a 5×5 biome lookup grid. + * + *

+ * The 5×5 grid is sampled at block offsets {0, 4, 8, 12, 16} in both X and Z + * (indices 0–4 per axis). Position 16 falls in the adjacent chunk — this is valid + * because ClimateSystem depends only on world coordinates, not on loaded chunk data. + * + *

+ * During cube generation, the per-column biome blend weights are bilinearly + * interpolated from the grid using {@code (bx/4, bz/4)} as the grid indices. + * + *

+ * Also used by canyon/trench edge blending to query neighbor-chunk biomes without + * requiring those chunks to be loaded. + */ +public class CCBiomeCache { + + /** Grid size: samples every 4 blocks → 5 sample points covers [0, 16] inclusive. */ + public static final int GRID_SIZE = 5; + public static final int GRID_STRIDE = 4; // blocks between grid samples + + 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 LinkedHashMap cache; + + public CCBiomeCache(ClimateSystem climate) { + this.climate = climate; + this.cache = new LinkedHashMap(CACHE_CAPACITY, 0.75f, true) { + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > CACHE_CAPACITY; + } + }; + } + + /** + * 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 = packKey(chunkX, chunkZ); + BiomeLookupResult[] grid = cache.get(key); + if (grid == null) { + grid = computeGrid(chunkX, chunkZ); + cache.put(key, grid); + } + return grid; + } + + private BiomeLookupResult[] computeGrid(int chunkX, int chunkZ) { + BiomeLookupResult[] grid = new BiomeLookupResult[GRID_SIZE * GRID_SIZE]; + int originX = chunkX * 16; + int originZ = chunkZ * 16; + for (int gi = 0; gi < GRID_SIZE; gi++) { + for (int gj = 0; gj < GRID_SIZE; gj++) { + double bx = originX + gi * GRID_STRIDE; + double bz = originZ + gj * GRID_STRIDE; + ClimatePoint cp = climate.sample(bx, bz); + grid[gi + gj * GRID_SIZE] = CCBiomeRegistry.lookup(cp.values, 3); + } + } + return grid; + } + + private static long packKey(int chunkX, int chunkZ) { + return ((long) chunkX << 32) | (chunkZ & 0xFFFFFFFFL); + } +} From ca60ef6ea11814d310090e50fde9ab331413a6c6 Mon Sep 17 00:00:00 2001 From: RecursivePineapple Date: Tue, 2 Jun 2026 15:52:08 -0400 Subject: [PATCH 6/8] terrain gen --- .../ccenhanced/CCEnhancedWorldGenerator.java | 100 +++++----- .../terrain/CCTerrainGenerator.java | 178 ++++++++++++++++++ .../ccenhanced/terrain/ColumnContext.java | 34 ++++ 3 files changed, 261 insertions(+), 51 deletions(-) create mode 100644 src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/terrain/CCTerrainGenerator.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/terrain/ColumnContext.java diff --git a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/CCEnhancedWorldGenerator.java b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/CCEnhancedWorldGenerator.java index f0539fde..86e786b9 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/CCEnhancedWorldGenerator.java +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/CCEnhancedWorldGenerator.java @@ -1,42 +1,57 @@ package com.cardinalstar.cubicchunks.worldgen.ccenhanced; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import javax.annotation.Nullable; import javax.annotation.ParametersAreNonnullByDefault; import net.minecraft.entity.EnumCreatureType; -import net.minecraft.init.Blocks; 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 com.cardinalstar.cubicchunks.api.ICube; import com.cardinalstar.cubicchunks.api.worldgen.GenerationResult; import com.cardinalstar.cubicchunks.api.worldgen.IWorldGenerator; import com.cardinalstar.cubicchunks.world.core.IColumnInternal; import com.cardinalstar.cubicchunks.world.cube.Cube; +import com.cardinalstar.cubicchunks.worldgen.ccenhanced.climate.ClimateSystem; +import com.cardinalstar.cubicchunks.worldgen.ccenhanced.terrain.CCBiomeCache; +import com.cardinalstar.cubicchunks.worldgen.ccenhanced.terrain.CCTerrainGenerator; +import com.cardinalstar.cubicchunks.worldgen.ccenhanced.terrain.ColumnContext; @ParametersAreNonnullByDefault public class CCEnhancedWorldGenerator implements IWorldGenerator { - private static final int SEA_LEVEL = 64; - - // Flat world layout: - // cubeY 0-2 (blocks 0-47): all stone - // cubeY 3 (blocks 48-63): stone, top layer (local y=15) = dirt - // cubeY 4 (blocks 64-79): grass at local y=0, air above - // cubeY 5+ (blocks 80+ ): air - private static final int SURFACE_CUBE_Y = SEA_LEVEL / 16; // 4 - private static final int DIRT_CUBE_Y = SURFACE_CUBE_Y - 1; // 3 + /** 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; + + // 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.contextCache = new LinkedHashMap(CONTEXT_CACHE_CAPACITY, 0.75f, true) { + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > CONTEXT_CACHE_CAPACITY; + } + }; } // ------------------------------------------------------------------------- @@ -48,9 +63,12 @@ public GenerationResult provideColumn(World world, int columnX, int colum Chunk chunk = new Chunk(world, columnX, columnZ); ((IColumnInternal) chunk).setColumn(true); + ColumnContext ctx = terrainGen.computeColumnContext(chunk, columnX, columnZ, biomeCache); + contextCache.put(packKey(columnX, columnZ), ctx); + List cubes = new ArrayList<>(16); - for (int cy = 0; cy < 16; cy++) { - cubes.add(new Cube(chunk, cy, createFlatEbs(cy))); + for (int cubeY = 0; cubeY < 16; cubeY++) { + cubes.add(new Cube(chunk, cubeY, terrainGen.buildEbs(world, ctx, cubeY))); } return new GenerationResult<>(chunk, null, cubes); @@ -65,22 +83,24 @@ public GenerationResult provideCube(@Nullable Chunk chunk, int cubeX, int List generatedColumns = new ArrayList<>(); List generatedCubes = new ArrayList<>(); - // Generate column + all vanilla-range cubes if chunk is missing, - // or if the requested cube falls in the vanilla range. + // 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); for (int cy = 0; cy < 16; cy++) { - generatedCubes.add(new Cube(chunk, cy, createFlatEbs(cy))); + generatedCubes.add(new Cube(chunk, cy, terrainGen.buildEbs(world, ctx, cy))); } } - // Cubes outside [0,16) are always air. + // Cubes outside [0, 16) are generated individually using the same terrain rules. if (cubeY < 0 || cubeY >= 16) { - generatedCubes.add(new Cube(chunk, cubeY, (ExtendedBlockStorage) null)); + 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. @@ -131,40 +151,18 @@ public ChunkPosition getNearestStructure(String name, int x, int y, int z) { // Helpers // ------------------------------------------------------------------------- - /** - * Build an ExtendedBlockStorage for the flat world at the given cube Y. - * Returns null for fully-air cubes (cubeY outside the filled range). - */ - @Nullable - private ExtendedBlockStorage createFlatEbs(int cubeY) { - if (cubeY < 0 || cubeY > SURFACE_CUBE_Y) { - return null; // air - } - - int yBase = cubeY * 16; - boolean storeSkylight = !world.provider.hasNoSky; - ExtendedBlockStorage ebs = new ExtendedBlockStorage(yBase, storeSkylight); - - for (int x = 0; x < 16; x++) { - for (int z = 0; z < 16; z++) { - if (cubeY < DIRT_CUBE_Y) { - // All stone - for (int ly = 0; ly < 16; ly++) { - ebs.func_150818_a(x, ly, z, Blocks.stone); - } - } else if (cubeY == DIRT_CUBE_Y) { - // Stone with dirt cap - for (int ly = 0; ly < 15; ly++) { - ebs.func_150818_a(x, ly, z, Blocks.stone); - } - ebs.func_150818_a(x, 15, z, Blocks.dirt); - } else { - // cubeY == SURFACE_CUBE_Y: grass at local y=0, air above - ebs.func_150818_a(x, 0, z, Blocks.grass); - } - } + /** 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; + } - return ebs; + private static long packKey(int chunkX, int chunkZ) { + return ((long) chunkX << 32) | (chunkZ & 0xFFFFFFFFL); } } 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..3dbdef1f --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/terrain/CCTerrainGenerator.java @@ -0,0 +1,178 @@ +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.ScaledNoise; +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 (SDD §4.3): + * + *

+ * surfaceY = SEA_LEVEL + blendedRoot * BASE_SCALE + blendedVar * hvNoise * VAR_SCALE
+ * 
+ * + * surfaceY is "first air Y": block at y < surfaceY is solid, block at y >= surfaceY is air. + */ +@ParametersAreNonnullByDefault +public class CCTerrainGenerator { + + // Heightmap formula constants + public static final int SEA_LEVEL = 64; + public static final float BASE_SCALE = 64.0f; + public static final float VAR_SCALE = 64.0f; + + /** Sum of amplitudes for 4 octaves: 2*(1 - 0.5^4) = 1.875. Used to normalize hvNoise. */ + private static final double HV_AMP_NORM = 2.0 * (1.0 - Math.pow(0.5, 4)); + + /** 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 ScaledNoise(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); + + // Pre-compute blended terrain params at each of the 25 grid points. + // This avoids calling blend() 256 times; only 25 blend computations needed. + float[] gridRoot = new float[CCBiomeCache.GRID_SIZE * CCBiomeCache.GRID_SIZE]; + float[] gridVar = new float[CCBiomeCache.GRID_SIZE * CCBiomeCache.GRID_SIZE]; + for (int gi = 0; gi < CCBiomeCache.GRID_SIZE; gi++) { + for (int gj = 0; gj < CCBiomeCache.GRID_SIZE; gj++) { + int idx = gi + gj * CCBiomeCache.GRID_SIZE; + 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[256]; + 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 gx = bx / CCBiomeCache.GRID_STRIDE; + int gz = bz / CCBiomeCache.GRID_STRIDE; + float fx = (bx % CCBiomeCache.GRID_STRIDE) / (float) CCBiomeCache.GRID_STRIDE; + float fz = (bz % CCBiomeCache.GRID_STRIDE) / (float) CCBiomeCache.GRID_STRIDE; + + float root = bilerp(gridRoot, gx, gz, fx, fz); + float var = bilerp(gridVar, gx, gz, fx, fz); + + double hv = hvNoise.sample(originX + bx, originZ + bz) / HV_AMP_NORM; + + int sy = (int) (SEA_LEVEL + root * BASE_SCALE + var * (float) hv * VAR_SCALE); + 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; + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + /** Bilinear interpolation over a flat GRID_SIZE×GRID_SIZE float array. */ + private static float bilerp(float[] grid, int gx, int gz, float fx, float fz) { + int g = CCBiomeCache.GRID_SIZE; + float v00 = grid[gx + gz * g]; + float v10 = grid[(gx + 1) + gz * g]; + float v01 = grid[gx + (gz + 1) * g]; + float v11 = grid[(gx + 1) + (gz + 1) * g]; + return v00 * (1 - fx) * (1 - fz) + v10 * fx * (1 - fz) + v01 * (1 - fx) * fz + v11 * fx * fz; + } +} 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]; + } +} From ef1be799ffd8e9901d9c0b0eaee93856e57ba9e7 Mon Sep 17 00:00:00 2001 From: RecursivePineapple Date: Fri, 3 Jul 2026 10:59:25 -0400 Subject: [PATCH 7/8] a few changes :innocent: --- .../cubicchunks/codegen/FieldType.java | 71 +++ .../cubicchunks/codegen/GenerateStructs.java | 41 ++ .../cubicchunks/codegen/Struct.java | 402 ++++++++++++++++ .../cubicchunks/codegen/StructField.java | 24 + .../cardinalstar/cubicchunks/CubicChunks.java | 8 +- .../hwaccel/AcceleratableWorldGenerator.java | 13 + .../api/worldgen/hwaccel/ComputeContext.java | 5 + .../api/worldgen/hwaccel/ComputePlan.java | 54 +++ .../api/worldgen/hwaccel/KernelBuilder.java | 17 + .../hwaccel/KernelConstantBuilder.java | 63 +++ .../api/worldgen/hwaccel/KernelContext.java | 140 ++++++ .../api/worldgen/hwaccel/KernelExecutor.java | 77 +++ .../api/worldgen/hwaccel/KernelJob.java | 13 + .../api/worldgen/hwaccel/KernelScheduler.java | 441 ++++++++++++++++++ .../worldgen/hwaccel/KernelSubmission.java | 11 + .../hwaccel/KernelSubmissionResult.java | 11 + .../hwaccel/KernelSubmissionToken.java | 8 + .../hwaccel/Noise2DKernelExecutor.java | 118 +++++ .../api/worldgen/hwaccel/PrimitiveBuffer.java | 57 +++ .../api/worldgen/hwaccel/PrimitiveView.java | 6 + .../api/worldgen/hwaccel/Terminal.java | 11 + .../api/worldgen/hwaccel/TerminalTask.java | 9 + .../api/worldgen/hwaccel/WorkDomain.java | 27 ++ .../hwaccel/buffer/ArenaHardwareBuffer.java | 119 +++++ .../hwaccel/buffer/BufferAccessor.java | 9 + .../hwaccel/buffer/BufferAllocator.java | 43 ++ .../hwaccel/buffer/BufferDataType.java | 17 + .../hwaccel/buffer/BufferDescriptor.java | 33 ++ .../buffer/ConstantHardwareBuffer.java | 114 +++++ .../worldgen/hwaccel/buffer/GPUBuffer.java | 102 ++++ .../hwaccel/buffer/HardwareBuffer.java | 81 ++++ .../hwaccel/buffer/HardwareSubBuffer.java | 76 +++ .../hwaccel/dag/ShaderDAGOperation.java | 7 + .../cubicchunks/async/CallingThread.java | 7 + .../cubicchunks/async/ThreadType.java | 7 + .../lighting/FirstLightProcessor.java | 303 ++++++------ .../cubicchunks/lighting/LightingManager.java | 20 +- .../lighting/phosphor/LightingHooks.java | 2 +- .../cubicchunks/mixin/Mixins.java | 18 + .../mixin/api/BlockExt_Lighting.java | 7 + .../early/common/AccessorNBTTagCompound.java | 17 + .../early/common/AccessorNBTTagList.java | 17 + .../early/common/MixinBlock_Lighting.java | 28 ++ .../mixin/early/common/MixinEBSID_EID.java | 45 ++ .../early/common/MixinEBSID_Vanilla.java | 59 +++ .../mixin/early/common/MixinWorld.java | 15 + .../early/common/MixinWorldProvider.java | 4 + .../early/common/MixinWorld_HeightLimit.java | 22 +- .../cubicchunks/mixin/ext/EBSIDAccessor.java | 8 + .../cubicchunks/network/CCPacketEntry.java | 1 + .../cubicchunks/network/NetworkChannel.java | 32 +- .../network/PacketEncoderCube.java | 10 +- .../PacketEncoderUpdateVisualizedBoxes.java | 74 +++ .../server/CubeProviderServer.java | 102 +++- .../server/CubicPlayerManager.java | 23 +- .../server/WorldSyncStateMachine.java | 4 + .../server/chunkio/CCNBTUtils.java | 105 ++++- .../server/chunkio/CubeLoaderServer.java | 76 ++- .../server/chunkio/ICubeLoader.java | 4 + .../server/chunkio/RegionCubeStorage.java | 2 +- .../util/CubeStatusVisualizer.java | 71 +-- .../cubicchunks/util/IntFraction.java | 67 +++ .../cubicchunks/util/JavaUtils.java | 24 + .../cubicchunks/util/MathUtil.java | 36 ++ .../util/boxvisualizer/BoxVisualizer.java | 29 ++ .../util/boxvisualizer/QuadSorter.java | 121 +++++ .../util/boxvisualizer/VisualizedBox.java | 34 ++ .../boxvisualizer/VisualizedBoxRenderer.java | 172 +++++++ .../cubicchunks/world/cube/Cube.java | 1 - .../cubicchunks/world/layer/LayeredWorld.java | 136 ++++++ .../cubicchunks/world/layer/ProxiedWorld.java | 140 ++++++ .../cubicchunks/world/layer/WorldLayer.java | 54 +++ .../world/worldgen/WorldGenerators.java | 6 +- .../worldgen/caves/NoodleCaveGenerator.java | 8 +- .../caves/SpaghettiCaveGenerator.java | 8 +- .../worldgen/noise/BlockNoiseSampler.java | 9 - .../world/worldgen/noise/NoiseSampler.java | 17 + .../worldgen/noise/NormalizedSampler.java | 76 +++ .../world/worldgen/noise/OctavesSampler.java | 67 ++- .../world/worldgen/noise/ScaledNoise.java | 30 -- .../world/worldgen/noise/ScaledSampler.java | 44 ++ .../worldgen/noise/SimplexNoiseSampler.java | 185 -------- .../world/worldgen/noise/SimplexSampler.java | 306 ++++++++++++ .../ccenhanced/CCEnhancedWorldGenerator.java | 349 +++++++++++++- .../biome/BiomeDistanceKernelExecutor.java | 131 ++++++ .../biome/BiomeLookupKernelExecutor.java | 142 ++++++ .../ccenhanced/biome/CCBiomeRegistry.java | 46 +- .../biome/HeightMapKernelExecutor.java | 164 +++++++ .../ccenhanced/biome/defaults/CCBiomes.java | 37 +- .../ccenhanced/climate/ClimateAxis.java | 11 +- .../ccenhanced/climate/ClimateSystem.java | 177 ++++++- .../ccenhanced/climate/NoiseClimateAxis.java | 54 +-- .../ccenhanced/surface/CCSurfacePainter.java | 100 ++++ .../ccenhanced/terrain/CCBiomeCache.java | 63 ++- .../terrain/CCTerrainGenerator.java | 62 +-- 95 files changed, 5530 insertions(+), 687 deletions(-) create mode 100644 src/buildSrc/java/com/cardinalstar/cubicchunks/codegen/FieldType.java create mode 100644 src/buildSrc/java/com/cardinalstar/cubicchunks/codegen/GenerateStructs.java create mode 100644 src/buildSrc/java/com/cardinalstar/cubicchunks/codegen/Struct.java create mode 100644 src/buildSrc/java/com/cardinalstar/cubicchunks/codegen/StructField.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/AcceleratableWorldGenerator.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/ComputeContext.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/ComputePlan.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelBuilder.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelConstantBuilder.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelContext.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelExecutor.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelJob.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelScheduler.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelSubmission.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelSubmissionResult.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelSubmissionToken.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/Noise2DKernelExecutor.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/PrimitiveBuffer.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/PrimitiveView.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/Terminal.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/TerminalTask.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/WorkDomain.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/ArenaHardwareBuffer.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/BufferAccessor.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/BufferAllocator.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/BufferDataType.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/BufferDescriptor.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/ConstantHardwareBuffer.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/GPUBuffer.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/HardwareBuffer.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/HardwareSubBuffer.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/dag/ShaderDAGOperation.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/async/CallingThread.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/async/ThreadType.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/mixin/api/BlockExt_Lighting.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/AccessorNBTTagCompound.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/AccessorNBTTagList.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/MixinBlock_Lighting.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/MixinEBSID_EID.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/MixinEBSID_Vanilla.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/mixin/ext/EBSIDAccessor.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/network/PacketEncoderUpdateVisualizedBoxes.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/util/IntFraction.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/util/JavaUtils.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/util/boxvisualizer/BoxVisualizer.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/util/boxvisualizer/QuadSorter.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/util/boxvisualizer/VisualizedBox.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/util/boxvisualizer/VisualizedBoxRenderer.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/world/layer/LayeredWorld.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/world/layer/ProxiedWorld.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/world/layer/WorldLayer.java delete mode 100644 src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/BlockNoiseSampler.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/NormalizedSampler.java delete mode 100644 src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/ScaledNoise.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/ScaledSampler.java delete mode 100644 src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/SimplexNoiseSampler.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/SimplexSampler.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/BiomeDistanceKernelExecutor.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/BiomeLookupKernelExecutor.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/HeightMapKernelExecutor.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/surface/CCSurfacePainter.java 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..07313d4b --- /dev/null +++ b/src/buildSrc/java/com/cardinalstar/cubicchunks/codegen/FieldType.java @@ -0,0 +1,71 @@ +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..1ec8e27b --- /dev/null +++ b/src/buildSrc/java/com/cardinalstar/cubicchunks/codegen/GenerateStructs.java @@ -0,0 +1,41 @@ +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..66e9d9de --- /dev/null +++ b/src/buildSrc/java/com/cardinalstar/cubicchunks/codegen/Struct.java @@ -0,0 +1,402 @@ +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..9c140c3e --- /dev/null +++ b/src/buildSrc/java/com/cardinalstar/cubicchunks/codegen/StructField.java @@ -0,0 +1,24 @@ +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 b96218e1..abd0d7f7 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; @@ -56,7 +57,6 @@ import com.falsepattern.chunk.api.DataRegistry; import com.gtnewhorizon.gtnhlib.config.ConfigException; import com.gtnewhorizon.gtnhlib.config.ConfigurationManager; - import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.ICrashCallable; import cpw.mods.fml.common.Loader; @@ -126,6 +126,12 @@ public void preInit(FMLPreInitializationEvent e) { 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/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/ComputeContext.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/ComputeContext.java new file mode 100644 index 00000000..1c66ddd1 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/ComputeContext.java @@ -0,0 +1,5 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; + +public interface ComputeContext { + +} 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..4d6cfb8c --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/ComputePlan.java @@ -0,0 +1,54 @@ +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.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 Map submit(KernelExecutor executor, Key key) { + return submit(executor, key, ImmutableMap.of()); + } + + 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..c7774158 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelBuilder.java @@ -0,0 +1,17 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; + +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.ConstantHardwareBuffer; + +public class KernelBuilder { + + private int discriminator = 0; + + public final StringBuilder preamble = new StringBuilder(); + public final StringBuilder logic = new StringBuilder(); + + public final ConstantHardwareBuffer constants = new ConstantHardwareBuffer("constants"); + + public String createName(String human) { + return human + "_" + (discriminator++); + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelConstantBuilder.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelConstantBuilder.java new file mode 100644 index 00000000..df1acce9 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelConstantBuilder.java @@ -0,0 +1,63 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; + +import java.util.ArrayList; +import java.util.List; + +public class KernelConstantBuilder { + + private final String type, name; + private final List lines = new ArrayList<>(); + + public KernelConstantBuilder(String type, String name) { + this.type = type; + this.name = name; + } + + public void annotate(String comment) { + lines.add(comment); + } + + public void add(float... values) { + lines.add(values); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + + int len = 0; + + for (var line : lines) { + if (line instanceof float[] values) { + len += values.length; + } + } + + builder.append("const ").append(type).append(" ").append(name).append("[").append(len).append("] = ").append(type).append("[").append(len).append("]("); + + boolean needsComma = false; + + for (var line : lines) { + if (line instanceof String comment) { + builder.append(" // ").append(comment).append("\n"); + } + + if (line instanceof float[] values) { + builder.append(" "); + + for (var value : values) { + if (needsComma) builder.append(","); + + builder.append(value).append("f"); + needsComma = true; + } + + builder.append("\n"); + } + } + + builder.append(");"); + + return builder.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..9a63ba36 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelContext.java @@ -0,0 +1,140 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; + +import static org.lwjgl.opengl.GL11.glGetInteger; +import static org.lwjgl.opengl.GL43.GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT; + +import org.lwjgl.opengl.GL11; +import org.lwjgl.sdl.SDLHints; +import org.lwjgl.sdl.SDLInit; +import org.lwjgl.sdl.SDLVideo; + +import com.cardinalstar.cubicchunks.CubicChunks; +import com.cardinalstar.cubicchunks.util.JavaUtils; +import cpw.mods.fml.common.FMLCommonHandler; +import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import cpw.mods.fml.common.gameevent.TickEvent.Phase; +import cpw.mods.fml.common.gameevent.TickEvent.RenderTickEvent; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import lombok.Getter; + +public class KernelContext { + + @Getter + private static boolean enabled; + + private static Thread worker; + + @Getter + private static KernelScheduler scheduler; + + @Getter + private static int SSBOAlignment; + + @SideOnly(Side.CLIENT) + public static void initClient() { + if (!isContextValid()) { + CubicChunks.LOGGER.info("Render context is invalid, disabling compute shaders"); + + return; + } + + CubicChunks.LOGGER.info("Render context is valid, using it as the compute context"); + + SSBOAlignment = glGetInteger(GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT); + + enabled = true; + + scheduler = new KernelScheduler(); + + FMLCommonHandler.instance().bus().register(new ClientEventHandler()); + } + + public static class ClientEventHandler { + + @SubscribeEvent + public void tick(RenderTickEvent event) { + if (event.phase != Phase.START) return; + + scheduler.tick(); + } + } + + @SideOnly(Side.SERVER) + public static void initServer() { + worker = new Thread(() -> { + SDLHints.SDL_SetHint("SDL_VIDEODRIVER", "offscreen"); + SDLInit.SDL_Init(SDLInit.SDL_INIT_VIDEO); + + long window = SDLVideo.SDL_CreateWindow("", 1, 1, SDLVideo.SDL_WINDOW_OPENGL | SDLVideo.SDL_WINDOW_HIDDEN); + long context = SDLVideo.SDL_GL_CreateContext(window); + SDLVideo.SDL_GL_MakeCurrent(window, context); + +// GL.create(SDLVideo::SDL_GL_GetProcAddress); +// GL.createCapabilities(); + + if (!isContextValid()) { + CubicChunks.LOGGER.info("Server offscreen context does not support compute shaders: disabling"); + return; + } + + CubicChunks.LOGGER.info("Successfully created server offscreen context that supports OpenGL compute shaders"); + + enabled = true; + + scheduler = new KernelScheduler(); + + try { + while (true) { + scheduler.tick(); + + Thread.yield(); + JavaUtils.onSpinWait(); // TODO: blocking + } + } catch (Throwable t) { + CubicChunks.LOGGER.error("CC-WG-Dispatcher failed with the following error", t); + } + + SDLVideo.SDL_GL_DestroyContext(context); + SDLVideo.SDL_DestroyWindow(window); + }); + + worker.setName("CC-WG-Dispatcher"); + worker.setDaemon(true); + + worker.start(); + } + + private static boolean isContextValid() { + String versionRaw = GL11.glGetString(GL11.GL_VERSION); + + CubicChunks.LOGGER.info("GL Version: {}", versionRaw); + + String[] version = versionRaw.split(" +"); + + String[] parts = version[0].split("\\."); + + if (Integer.parseInt(parts[0]) >= 4 && Integer.parseInt(parts[1]) >= 3) return true; + + String extensionsRaw = GL11.glGetString(GL11.GL_EXTENSIONS); + + CubicChunks.LOGGER.info("GL Extensions: {}", extensionsRaw); + + String[] extensions = extensionsRaw.split(" +"); + + boolean hasComputeShaders = false; + boolean hasSSBOs = false; + + for (var ext : extensions) { + if (ext.equals("GL_ARB_compute_shader")) { + hasComputeShaders = true; + } + + if (ext.equals("GL_ARB_shader_storage_buffer_object")) { + hasSSBOs = true; + } + } + + return hasComputeShaders && hasSSBOs; + } +} 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..e7f1c8ae --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelExecutor.java @@ -0,0 +1,77 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; + +import static org.lwjgl.opengl.GL20.GL_COMPILE_STATUS; +import static org.lwjgl.opengl.GL20.GL_LINK_STATUS; +import static org.lwjgl.opengl.GL20.glAttachShader; +import static org.lwjgl.opengl.GL20.glCompileShader; +import static org.lwjgl.opengl.GL20.glCreateProgram; +import static org.lwjgl.opengl.GL20.glCreateShader; +import static org.lwjgl.opengl.GL20.glGetProgramInfoLog; +import static org.lwjgl.opengl.GL20.glGetProgrami; +import static org.lwjgl.opengl.GL20.glGetShaderInfoLog; +import static org.lwjgl.opengl.GL20.glGetShaderi; +import static org.lwjgl.opengl.GL20.glLinkProgram; +import static org.lwjgl.opengl.GL20.glShaderSource; +import static org.lwjgl.opengl.GL43.GL_COMPUTE_SHADER; + +import java.util.Map; + +import net.minecraft.util.MathHelper; + +import com.cardinalstar.cubicchunks.CubicChunks; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferAllocator; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferDescriptor; + +public interface KernelExecutor { + + Map getOutputs(ComputePlan plan, KernelSubmissionToken submission, Key key, Map inputs); + + KernelSubmissionResult[] submit(BufferAllocator alloc, KernelSubmission[] submissions); + + static int createProgram(String code) { + int shader = glCreateShader(GL_COMPUTE_SHADER); + glShaderSource(shader, code); + glCompileShader(shader); + + if (glGetShaderi(shader, GL_COMPILE_STATUS) == 0) { + String log = glGetShaderInfoLog(shader, 32000); + + String[] lines = code.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: {}", log, new Exception()); + throw new RuntimeException("Could not compile shader"); + } + + int program = glCreateProgram(); + + glAttachShader(program, shader); + glLinkProgram(program); + + if (glGetProgrami(program, GL_LINK_STATUS) == 0) { + String log = glGetProgramInfoLog(program, 32000); + + String[] lines = code.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 link shader: {}", log, new Exception()); + throw new RuntimeException("Could not link shader"); + } + + return program; + } +} 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..5e75749a --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelJob.java @@ -0,0 +1,13 @@ +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..b1b26fec --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelScheduler.java @@ -0,0 +1,441 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; + +import static org.lwjgl.opengl.ARBSync.glClientWaitSync; +import static org.lwjgl.opengl.ARBSync.glDeleteSync; +import static org.lwjgl.opengl.ARBSync.glFenceSync; +import static org.lwjgl.opengl.GL11.glFlush; +import static org.lwjgl.opengl.GL11.glGetError; +import static org.lwjgl.opengl.GL30.glBindBufferBase; +import static org.lwjgl.opengl.GL32.GL_ALREADY_SIGNALED; +import static org.lwjgl.opengl.GL32.GL_SYNC_GPU_COMMANDS_COMPLETE; +import static org.lwjgl.opengl.GL42.glMemoryBarrier; +import static org.lwjgl.opengl.GL43.GL_SHADER_STORAGE_BARRIER_BIT; +import static org.lwjgl.opengl.GL43.GL_SHADER_STORAGE_BUFFER; +import static org.lwjgl.opengl.GL44.GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.lwjgl.opengl.GLSync; +import org.lwjgl.util.glu.GLU; + +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.ArenaHardwareBuffer; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferAllocator; +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.GPUBuffer; +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.Int2ObjectMap.Entry; +import it.unimi.dsi.fastutil.objects.Object2IntMaps; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; + +@SuppressWarnings("rawtypes") +public class KernelScheduler { + + @Desugar + private record TerminalResult(Map data, TerminalTask task) {} + + // 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, Lock lock, Condition condition) 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 Runnable pollForTasks = this::poll; + private Runnable state = pollForTasks; + + private final ArenaHardwareBuffer arena = new ArenaHardwareBuffer(); + + // ------------------------------------------------------------------------- + // 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) { + Lock lock = new ReentrantLock(); + Condition condition = lock.newCondition(); + tasks.add(new ExecTask(fn, lock, condition)); + try { + lock.lock(); + condition.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + lock.unlock(); + } + } + + @CallingThread(ThreadType.SERVER) + public void processResults() { + TerminalResult result; + + while ((result = results.poll()) != null) { + result.task().execute(result.data()); + } + } + + @CallingThread(ThreadType.CLIENT) + public void tick() { + state.run(); + } + + @CallingThread(ThreadType.CLIENT) + private void poll() { + Task task; + + while ((task = this.tasks.poll()) != null) { + if (task instanceof SubmitTask submit) { + this.submitPlans(submit.plans, submit.ready); + } else if (task instanceof ExecTask exec) { + exec.fn().run(); + try { + exec.lock().lock(); + exec.condition().signalAll(); + } finally { + exec.lock().unlock(); + } + } + } + } + + @CallingThread(ThreadType.CLIENT) + private void submitPlans(List plans, AtomicBoolean ready) { + LOGGER.info("Processing batch with {} plans", plans.size()); + + // --- Step A: build the global job table --- + // + // Each plan's submission IDs start at 0. We assign a planBase (cumulative submission + // count) and bufferBase (cumulative buffer-slot count) so that every job and every + // buffer slot has a globally unique index across the whole batch. + 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(), KernelContext.getSSBOAlignment()); + } + } + + checkError(); + + // --- Step C: wave dispatch --- + // + // All jobs at the same level are independent of each other and can be dispatched + // without a barrier between them. A single barrier is issued between consecutive + // levels. This amortises the barrier cost across all plans in the batch: for N chunks + // the pipeline uses 6 barriers total instead of 6 per chunk. + + arenaLength *= 2; + + if (this.arena.getByteLen() < arenaLength) { + this.arena.resize(arenaLength); + } else { + this.arena.reset(); + } + + Allocator alloc = new Allocator(arena); + + GPUBuffer[] buffers = new GPUBuffer[totalBuffers]; + + // Track how many real (non-cached) dispatches each executor made, for EMA timing. + Object2IntOpenHashMap dispatchCounts = new Object2IntOpenHashMap<>(); + + long batchStart = System.nanoTime(); + + 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) -> { + KernelSubmission[] submissionArray = DataUtils.mapToArray(submissionList, KernelSubmission[]::new, Pair::right); + + checkError("pre " + exec.toString()); + + @SuppressWarnings("unchecked") + KernelSubmissionResult[] results = exec.submit(alloc, submissionArray); + + checkError("post " + exec.toString()); + + 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()); + buffers[pair.left().bufferBase() + outDesc.bufferId()] = e.getValue(); + } + } + }); + + glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); + } + + GLSync sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + glFlush(); + + checkError(); + + this.state = () -> { + if (glClientWaitSync(sync, 0, 0) == GL_ALREADY_SIGNALED) { + checkError(); + + this.state = pollForTasks; + + glDeleteSync(sync); + + checkError(); + + long batchElapsed = System.nanoTime() - batchStart; + updateEMAs(dispatchCounts, batchElapsed); + + LOGGER.info("Batch took {} ms", batchElapsed / 1000000); + + // 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 = resolveDownloads(terminal.inputs(), bb, buffers, downloads); + this.results.add(new TerminalResult(inputs, terminal.task())); + } + } + + checkError(); + + ready.set(true); + } + }; + } + + private void checkError() { + int error = glGetError(); + + if (error != 0) { + LOGGER.error("GL Error: {}", GLU.gluErrorString(error), new Exception()); + } + } + + private void checkError(String info) { + int error = glGetError(); + + if (error != 0) { + LOGGER.error("GL Error ({}): {}", info, GLU.gluErrorString(error), new Exception()); + } + } + + /// Copies any not-yet-downloaded buffers referenced by `terminalInputs` from the mapped arena + /// into fresh direct ByteBuffers and returns them. + @CallingThread(ThreadType.CLIENT) + private Map resolveDownloads( + Map terminalInputs, + int bufferBaseOffset, + GPUBuffer[] buffers, + ByteBuffer[] downloads + ) { + Map out = new HashMap<>(); + + for (var input : terminalInputs.entrySet()) { + int localBufId = input.getValue().bufferId(); + + if (downloads[localBufId] == null) { + GPUBuffer buf = buffers[bufferBaseOffset + localBufId]; + + ByteBuffer downloaded = ByteBuffer.allocateDirect(buf.getBufferLength()).order(ByteOrder.nativeOrder()); + + buf.download(downloaded); + + downloads[localBufId] = downloaded; + } + + 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 static class Allocator implements BufferAllocator { + + /// The primary arena buffer, which will be closed after all plans have completed. + public final ArenaHardwareBuffer arena; + + public Allocator(ArenaHardwareBuffer arena) { + this.arena = arena; + } + + @Override + public GPUBuffer alloc(BufferDataType dataType, int lenX, int lenY, int lenZ) { + GPUBuffer subbuffer = arena.alloc(dataType, lenX, lenY, lenZ); + + if (subbuffer != null) return subbuffer; + + throw new IllegalStateException("Arena GPU buffer overflowed: something is allocating more memory than it planned for"); + } + + @Override + public void bindSSBO(int index) { + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, index, arena.getSsbo()); + } + + @Override + public void unbindSSBO(int index) { + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, index, 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..06086142 --- /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..25331b52 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/Noise2DKernelExecutor.java @@ -0,0 +1,118 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; + +import static org.lwjgl.opengl.GL20.glUseProgram; +import static org.lwjgl.opengl.GL43.glDispatchCompute; + +import java.util.Map; + +import net.minecraft.world.ChunkCoordIntPair; + +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferAllocator; +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.ConstantHardwareBuffer; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.GPUBuffer; +import com.cardinalstar.cubicchunks.world.worldgen.noise.NoiseSampler; +import com.google.common.collect.ImmutableMap; + +public class Noise2DKernelExecutor implements KernelExecutor { + + private final int program; + + private final ConstantHardwareBuffer constants; + + public Noise2DKernelExecutor(NoiseSampler sampler) { + KernelBuilder builder = new KernelBuilder(); + + String result2d = sampler.compileKernel2D(builder, "gx", "gz"); + + String code2 = + """ + #version 430 core + + layout(local_size_x = 16, local_size_z = 16) in; + + $uniform + + layout(std430, binding = 0) writeonly buffer Arena { float arena[]; }; + layout(std430, binding = 1) readonly buffer Uniforms { Noise2DUniform uniforms[]; }; + layout(std430, binding = 2) readonly buffer Constants { uint constants[]; }; + + $preamble + + void main() { + uint id = gl_WorkGroupID.x; + + uint x = gl_LocalInvocationID.x; + uint z = gl_LocalInvocationID.z; + + int offsetX = uniforms[id].offsetX; + int offsetZ = uniforms[id].offsetZ; + uint outputOffset = uniforms[id].outputOffset; + + int gx = offsetX + int(x); + int gz = offsetZ + int(z); + + $logic + + arena[(z << 4) + x + outputOffset] = $result; + } + """; + + code2 = code2.replace("$preamble", builder.preamble.toString()) + .replace("$logic", builder.logic.toString()) + .replace("$result", result2d) + .replace("$uniform", Noise2DUniformGLStruct.SOURCE); + + this.program = KernelExecutor.createProgram(code2); + this.constants = builder.constants.finish(); + } + + @Override + public Map getOutputs( + ComputePlan plan, + KernelSubmissionToken submission, + ChunkCoordIntPair key, + Map inputs + ) { + return ImmutableMap.of("noise", plan.describeBuffer(submission, BufferDataType.f32, 16, 16)); + } + + @Override + public KernelSubmissionResult[] submit(BufferAllocator alloc, KernelSubmission[] submissions) { + glUseProgram(this.program); + + Noise2DUniformPrimitiveBuffer uniforms = new Noise2DUniformPrimitiveBuffer(submissions.length); + + KernelSubmissionResult[] results = new KernelSubmissionResult[submissions.length]; + + uniforms.forEachFast((i, view) -> { + var key = submissions[i].key(); + + GPUBuffer output = alloc.alloc(BufferDataType.f32, 16, 16); + results[i] = new KernelSubmissionResult(ImmutableMap.of("noise", output)); + + view.setOffsetX(key.chunkXPos << 4); + view.setOffsetZ(key.chunkZPos << 4); + view.setOutputOffset(output.getBufferOffset() / 4); + + return true; + }); + + GPUBuffer uniformGPU = alloc.uniform(uniforms); + + alloc.bindSSBO(0); + uniformGPU.bind(1); + constants.bindSSBO(2); + + glDispatchCompute(submissions.length, 1, 1); + + alloc.unbindSSBO(0); + uniformGPU.unbind(1); + constants.unbindSSBO(2); + + glUseProgram(0); + + return results; + } +} 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..cefbd1f2 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/PrimitiveBuffer.java @@ -0,0 +1,57 @@ +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/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/WorkDomain.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/WorkDomain.java new file mode 100644 index 00000000..2be53215 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/WorkDomain.java @@ -0,0 +1,27 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; + +import com.cardinalstar.cubicchunks.util.IntFraction; + +public class WorkDomain { + + public int samplesX = 1, samplesY = 1, samplesZ = 1; + + public IntFraction scaleX = new IntFraction(), scaleY = new IntFraction(), scaleZ = new IntFraction(); + + public WorkDomain() { + } + + public WorkDomain(int samplesX, int samplesY, int samplesZ) { + this.samplesX = samplesX; + this.samplesY = samplesY; + this.samplesZ = samplesZ; + } + + public static WorkDomain chunk() { + return new WorkDomain(16, 1, 16); + } + + public static WorkDomain cube() { + return new WorkDomain(16, 16, 16); + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/ArenaHardwareBuffer.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/ArenaHardwareBuffer.java new file mode 100644 index 00000000..fe03e33b --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/ArenaHardwareBuffer.java @@ -0,0 +1,119 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer; + +import static org.lwjgl.opengl.GL15.glBindBuffer; +import static org.lwjgl.opengl.GL15.glDeleteBuffers; +import static org.lwjgl.opengl.GL15.glGenBuffers; +import static org.lwjgl.opengl.GL15.glUnmapBuffer; +import static org.lwjgl.opengl.GL30.GL_MAP_FLUSH_EXPLICIT_BIT; +import static org.lwjgl.opengl.GL30.GL_MAP_INVALIDATE_BUFFER_BIT; +import static org.lwjgl.opengl.GL30.GL_MAP_READ_BIT; +import static org.lwjgl.opengl.GL30.GL_MAP_WRITE_BIT; +import static org.lwjgl.opengl.GL30.glFlushMappedBufferRange; +import static org.lwjgl.opengl.GL30.glMapBufferRange; +import static org.lwjgl.opengl.GL43.GL_SHADER_STORAGE_BUFFER; +import static org.lwjgl.opengl.GL44.GL_DYNAMIC_STORAGE_BIT; +import static org.lwjgl.opengl.GL44.GL_MAP_PERSISTENT_BIT; +import static org.lwjgl.opengl.GL44.glBufferStorage; + +import java.io.Closeable; +import java.nio.ByteBuffer; + +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelContext; +import com.cardinalstar.cubicchunks.util.MathUtil; +import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities; +import lombok.Getter; + +public class ArenaHardwareBuffer implements Closeable { + + @Getter + private int ssbo = 0; + @Getter + private int byteLen = 0; + + private int nextBufferOffset; + + @Getter + private boolean open = true; + + @Getter + private ByteBuffer mapped; + + public void download(ByteBuffer dest, int offset, int len) { + if (offset + len > byteLen) { + throw new IllegalArgumentException("Invalid parameters: offset=" + offset + ", len=" + len + ", byteLen=" + byteLen); + } + + if (len > dest.remaining()) { + throw new IllegalArgumentException("Invalid parameters: remaining=" + dest.remaining() + ", len=" + len); + } + + MemoryUtilities.memCopy(MemoryUtilities.memAddress(mapped) + offset, MemoryUtilities.memAddress(dest), len); + } + + public void upload(ByteBuffer src, int offset, int len) { + if (offset + len > byteLen) { + throw new IllegalArgumentException("Invalid parameters: offset=" + offset + ", len=" + len + ", byteLen=" + byteLen); + } + + if (len > src.remaining()) { + throw new IllegalArgumentException("Invalid parameters: remaining=" + src.remaining() + ", len=" + len); + } + + MemoryUtilities.memCopy(MemoryUtilities.memAddress(src), MemoryUtilities.memAddress(mapped) + offset, len); + + glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo); + glFlushMappedBufferRange(GL_SHADER_STORAGE_BUFFER, offset, len); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + } + + public void reset() { + this.nextBufferOffset = 0; + } + + public void resize(int byteLen) { + if (this.ssbo != 0) { + close(); + } + + this.ssbo = glGenBuffers(); + + int storageFlags = GL_DYNAMIC_STORAGE_BIT | GL_MAP_READ_BIT | GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT; + int mapFlags = GL_MAP_READ_BIT | GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_FLUSH_EXPLICIT_BIT; + + glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo); + glBufferStorage(GL_SHADER_STORAGE_BUFFER, byteLen, storageFlags); + this.mapped = glMapBufferRange(GL_SHADER_STORAGE_BUFFER, 0, byteLen, mapFlags, null); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + + this.open = true; + this.byteLen = byteLen; + } + + @Override + public void close() { + glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo); + glUnmapBuffer(GL_SHADER_STORAGE_BUFFER); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + + glDeleteBuffers(this.ssbo); + + this.ssbo = 0; + this.open = false; + this.mapped = null; + this.byteLen = 0; + this.nextBufferOffset = 0; + } + + public HardwareSubBuffer alloc(BufferDataType dataType, int lenX, int lenY, int lenZ) { + int byteLen = dataType.width() * lenX * lenY * lenZ; + + if (this.nextBufferOffset + byteLen > this.byteLen) { + return null; + } + + int offset = this.nextBufferOffset; + this.nextBufferOffset = MathUtil.alignTo(this.nextBufferOffset + byteLen, KernelContext.getSSBOAlignment()); + + return new HardwareSubBuffer(this, offset, dataType, lenX, lenY, lenZ); + } +} 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..4e1669c4 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/BufferAccessor.java @@ -0,0 +1,9 @@ +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 { + + 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..11ae3548 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/BufferAllocator.java @@ -0,0 +1,43 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer; + +import java.nio.ByteBuffer; + +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.PrimitiveBuffer; +import com.cardinalstar.cubicchunks.util.MathUtil; +import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities; + +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 uniform(PrimitiveBuffer buffer) { + int byteLen = buffer.getByteLength(); + int wordLen = MathUtil.ceilDiv(byteLen, 4); + + GPUBuffer gpu = alloc(BufferDataType.i32, wordLen); + + ByteBuffer temp = MemoryUtilities.memAlloc(byteLen); + + try { + buffer.upload(temp); + temp.flip(); + + gpu.upload(temp); + } finally { + MemoryUtilities.memFree(temp); + } + + return gpu; + } + + GPUBuffer alloc(BufferDataType dataType, int lenX, int lenY, int lenZ); + + void bindSSBO(int index); + void unbindSSBO(int index); +} 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..448aa1ba --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/BufferDataType.java @@ -0,0 +1,17 @@ +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; + }; + } +} 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..dcdc394b --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/BufferDescriptor.java @@ -0,0 +1,33 @@ +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 int getBufferLength() { + return dataType.width() * lenX * lenY * lenZ; + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/ConstantHardwareBuffer.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/ConstantHardwareBuffer.java new file mode 100644 index 00000000..1e6a785d --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/ConstantHardwareBuffer.java @@ -0,0 +1,114 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer; + +import static org.lwjgl.opengl.GL15.GL_STATIC_DRAW; +import static org.lwjgl.opengl.GL15.glBindBuffer; +import static org.lwjgl.opengl.GL15.glBufferData; +import static org.lwjgl.opengl.GL15.glDeleteBuffers; +import static org.lwjgl.opengl.GL15.glGenBuffers; +import static org.lwjgl.opengl.GL30.glBindBufferBase; +import static org.lwjgl.opengl.GL43.GL_SHADER_STORAGE_BUFFER; + +import java.io.Closeable; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelContext; +import com.cardinalstar.cubicchunks.util.MathUtil; +import com.github.bsideup.jabel.Desugar; +import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities; +import lombok.Getter; + +public class ConstantHardwareBuffer implements Closeable { + + public final int ssbo; + private final String bufferName; + + @Getter + private boolean open = true; + private boolean finished = false; + + private final List ranges = new ArrayList<>(); + private int len; + + @Desugar + private record Range(int start, int size, long ptr) { } + + public ConstantHardwareBuffer(String bufferName) { + this.ssbo = glGenBuffers(); + this.bufferName = bufferName; + } + + public BufferAccessor append(BufferDataType dataType, ByteBuffer data) { + if (dataType.width() != 4) throw new IllegalArgumentException("Constant buffer can only store data types whose byte length is exactly 4 bytes"); + + int size = data.remaining(); + + long ptr = MemoryUtilities.nmemAlloc(data.remaining()); + MemoryUtilities.memByteBuffer(ptr, size).put(data); + + int offset = len; + + ranges.add(new Range(offset, size, ptr)); + + len = MathUtil.alignTo(len + size, KernelContext.getSSBOAlignment()); + + return index -> { + String base = String.format("%s[(%s) + %d]", this.bufferName, index, offset / 4); + + return switch (dataType) { + case i32 -> "(int(" + base + "))"; + case u32 -> "(" + base + ")"; + case f32 -> "(uintBitsToFloat(" + base + "))"; + default -> throw new AssertionError(); + }; + }; + } + + @Override + public void close() { + for (var range : ranges) { + MemoryUtilities.nmemFree(range.ptr); + } + + ranges.clear(); + + this.open = false; + glDeleteBuffers(this.ssbo); + } + + public ConstantHardwareBuffer finish() { + if (finished) throw new IllegalStateException("Cannot finish ConstantHardwareBuffer twice"); + finished = true; + + ByteBuffer data = MemoryUtilities.memAlloc(len); + long dataPtr = MemoryUtilities.memAddress(data); + + for (var range : ranges) { + MemoryUtilities.memCopy(range.ptr, dataPtr, range.size); + MemoryUtilities.nmemFree(range.ptr); + } + + ranges.clear(); + + glBindBuffer(GL_SHADER_STORAGE_BUFFER, this.ssbo); + glBufferData(GL_SHADER_STORAGE_BUFFER, data, GL_STATIC_DRAW); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + + MemoryUtilities.memFree(data); + + return this; + } + + public void bindSSBO(int index) { + if (!finished) throw new IllegalStateException("Cannot bind unfinished ConstantHardwareBuffer"); + + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, index, this.ssbo); + } + + public void unbindSSBO(int index) { + if (!finished) throw new IllegalStateException("Cannot bind unfinished ConstantHardwareBuffer"); + + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, index, 0); + } +} 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..f3d33614 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/GPUBuffer.java @@ -0,0 +1,102 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer; + +import static org.lwjgl.opengl.GL11.glGetError; +import static org.lwjgl.opengl.GL15.glBindBuffer; +import static org.lwjgl.opengl.GL15.glUnmapBuffer; +import static org.lwjgl.opengl.GL30.GL_MAP_READ_BIT; +import static org.lwjgl.opengl.GL30.GL_MAP_WRITE_BIT; +import static org.lwjgl.opengl.GL30.glMapBufferRange; +import static org.lwjgl.opengl.GL31.GL_COPY_READ_BUFFER; +import static org.lwjgl.opengl.GL31.GL_COPY_WRITE_BUFFER; +import static org.lwjgl.opengl.GL31.glCopyBufferSubData; +import static org.lwjgl.opengl.GL43.GL_SHADER_STORAGE_BUFFER; + +import java.io.Closeable; +import java.nio.ByteBuffer; + +import org.lwjgl.util.glu.GLU; + +import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities; + +public interface GPUBuffer extends Closeable { + + BufferDataType getDataType(); + + int getBufferId(); + int getBufferOffset(); + default int getBufferLength() { + return getDataType().width() * getLenX() * getLenY() * getLenZ(); + } + + int getLenX(); + int getLenY(); + int getLenZ(); + + void bind(int index); + void unbind(int index); + + default void copyTo(GPUBuffer dest) { + if (this.getBufferLength() != dest.getBufferLength()) { + throw new UnsupportedOperationException("Both buffers must be the same length when copying"); + } + + glBindBuffer(GL_COPY_READ_BUFFER, this.getBufferId()); + glBindBuffer(GL_COPY_WRITE_BUFFER, dest.getBufferId()); + + glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, this.getBufferOffset(), dest.getBufferOffset(), this.getBufferLength()); + + glBindBuffer(GL_COPY_READ_BUFFER, 0); + glBindBuffer(GL_COPY_WRITE_BUFFER, 0); + } + + default void download(ByteBuffer dest) { + if (this.getBufferLength() != dest.remaining()) { + throw new UnsupportedOperationException("Both buffers must be the same length when copying"); + } + + glBindBuffer(GL_SHADER_STORAGE_BUFFER, getBufferId()); + ByteBuffer mapped = glMapBufferRange(GL_SHADER_STORAGE_BUFFER, getBufferOffset(), getBufferLength(), GL_MAP_READ_BIT, null); + + if (mapped == null) { + int error = glGetError(); + + glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + + throw new IllegalStateException("Could not map buffer: " + GLU.gluErrorString(error)); + } + + MemoryUtilities.memCopy(mapped, dest); + dest.position(dest.position() + this.getBufferLength()); + + glUnmapBuffer(GL_SHADER_STORAGE_BUFFER); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + } + + default void upload(ByteBuffer data) { + if (this.getBufferLength() != data.remaining()) { + throw new UnsupportedOperationException("Both buffers must be the same length when copying"); + } + + glBindBuffer(GL_SHADER_STORAGE_BUFFER, getBufferId()); + ByteBuffer mapped = glMapBufferRange(GL_SHADER_STORAGE_BUFFER, getBufferOffset(), getBufferLength(), GL_MAP_WRITE_BIT, null); + + if (mapped == null) { + int error = glGetError(); + + glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + + throw new IllegalStateException("Could not map buffer: " + GLU.gluErrorString(error)); + } + + MemoryUtilities.memCopy(data, mapped); + data.position(data.position() + this.getBufferLength()); + + glUnmapBuffer(GL_SHADER_STORAGE_BUFFER); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + } + + @Override + void close(); + + boolean shouldBeCached(); +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/HardwareBuffer.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/HardwareBuffer.java new file mode 100644 index 00000000..7020099f --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/HardwareBuffer.java @@ -0,0 +1,81 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer; + +import static org.lwjgl.opengl.GL15.GL_DYNAMIC_DRAW; +import static org.lwjgl.opengl.GL15.glBindBuffer; +import static org.lwjgl.opengl.GL15.glBufferData; +import static org.lwjgl.opengl.GL15.glDeleteBuffers; +import static org.lwjgl.opengl.GL15.glGenBuffers; +import static org.lwjgl.opengl.GL30.glBindBufferBase; +import static org.lwjgl.opengl.GL43.GL_SHADER_STORAGE_BUFFER; + +import java.io.Closeable; + +import lombok.Getter; +import lombok.Setter; + +public class HardwareBuffer implements Closeable, GPUBuffer { + + public final int ssbo; + + @Getter + public final BufferDataType dataType; + @Getter + public final int lenX, lenY, lenZ; + + private boolean open = true; + @Setter + private boolean shouldBeCached; + + public HardwareBuffer(int ssbo, BufferDataType dataType, int lenX, int lenY, int lenZ) { + this.ssbo = ssbo; + this.dataType = dataType; + this.lenX = lenX; + this.lenY = lenY; + this.lenZ = lenZ; + } + + @Override + public int getBufferId() { + return ssbo; + } + + @Override + public int getBufferOffset() { + return 0; + } + + @Override + public void bind(int index) { + if (!open) { + throw new IllegalStateException("Cannot bind closed HardwareBuffer"); + } + + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, index, this.ssbo); + } + + @Override + public void unbind(int index) { + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, index, 0); + } + + public static HardwareBuffer alloc(BufferDataType dataType, int lenX, int lenY, int lenZ) { + int ssbo = glGenBuffers(); + + glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo); + glBufferData(GL_SHADER_STORAGE_BUFFER, (long) dataType.width() * lenX * lenY * lenZ, GL_DYNAMIC_DRAW); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + + return new HardwareBuffer(ssbo, dataType, lenX, lenY, lenZ); + } + + @Override + public void close() { + this.open = false; + glDeleteBuffers(this.ssbo); + } + + @Override + public boolean shouldBeCached() { + return shouldBeCached; + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/HardwareSubBuffer.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/HardwareSubBuffer.java new file mode 100644 index 00000000..73e4eeea --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/HardwareSubBuffer.java @@ -0,0 +1,76 @@ +package com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer; + +import static org.lwjgl.opengl.GL30.glBindBufferBase; +import static org.lwjgl.opengl.GL30.glBindBufferRange; +import static org.lwjgl.opengl.GL43.GL_SHADER_STORAGE_BUFFER; + +import java.nio.ByteBuffer; + +import lombok.Getter; + +public class HardwareSubBuffer implements GPUBuffer { + + private final ArenaHardwareBuffer arena; + public final int ssbo; + + public final int ssboOffset; + + @Getter + public final BufferDataType dataType; + @Getter + public final int lenX, lenY, lenZ; + + public HardwareSubBuffer(ArenaHardwareBuffer arena, int ssboOffset, BufferDataType dataType, int lenX, int lenY, int lenZ) { + this.arena = arena; + this.ssbo = arena.getSsbo(); + this.ssboOffset = ssboOffset; + this.dataType = dataType; + this.lenX = lenX; + this.lenY = lenY; + this.lenZ = lenZ; + } + + @Override + public int getBufferId() { + return ssbo; + } + + @Override + public int getBufferOffset() { + return ssboOffset; + } + + @Override + public void bind(int index) { + if (!arena.isOpen()) { + throw new IllegalStateException("Cannot bind HardwareSubBuffer when its arena buffer has been closed"); + } + + glBindBufferRange(GL_SHADER_STORAGE_BUFFER, index, this.ssbo, this.ssboOffset, this.getBufferLength()); + } + + @Override + public void unbind(int index) { + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, index, 0); + } + + @Override + public void close() { + // do nothing + } + + @Override + public boolean shouldBeCached() { + return false; + } + + @Override + public void download(ByteBuffer dest) { + this.arena.download(dest, getBufferOffset(), getBufferLength()); + } + + @Override + public void upload(ByteBuffer data) { + this.arena.upload(data, getBufferOffset(), getBufferLength()); + } +} 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/lighting/FirstLightProcessor.java b/src/main/java/com/cardinalstar/cubicchunks/lighting/FirstLightProcessor.java index ba88f37e..892ee758 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/lighting/FirstLightProcessor.java +++ b/src/main/java/com/cardinalstar/cubicchunks/lighting/FirstLightProcessor.java @@ -24,25 +24,23 @@ 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 +55,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 +62,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); + + ExtendedBlockStorage storage = cube.getStorage(); - 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()); + 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,132 +124,121 @@ 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( + diffuseSkylightInBlockColumn( lm, - otherCube, - this.curPosX, - this.curPosY, - this.curPosZ, + affectedCube, + lX + bX, + lZ + bZ, minBlockY, - maxBlockY)) { - throw new IllegalStateException( - "Check light failed at (" + this.curPosX - + ", " - + this.curPosY - + ", " - + this.curPosZ - + ")" - + "!"); - } + 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, + 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()); @@ -245,29 +246,30 @@ private boolean diffuseSkylightInBlockColumn(ILightingManager lm, ICube cube, in 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 +279,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 +288,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..a13bf347 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,15 @@ 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..d870e801 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/lighting/phosphor/LightingHooks.java +++ b/src/main/java/com/cardinalstar/cubicchunks/lighting/phosphor/LightingHooks.java @@ -55,7 +55,7 @@ private static void scheduleRelightChecksForColumn(final World world, final Enum scheduleRelightChecksForColumn( world, - world.getChunkFromBlockCoords(blockToCube(x), blockToCube(z)), + world.getChunkFromBlockCoords(x, z), lightType, x, z, diff --git a/src/main/java/com/cardinalstar/cubicchunks/mixin/Mixins.java b/src/main/java/com/cardinalstar/cubicchunks/mixin/Mixins.java index 7a31c4af..a4759101 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/mixin/Mixins.java +++ b/src/main/java/com/cardinalstar/cubicchunks/mixin/Mixins.java @@ -94,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.") 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..445ae7ae --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/MixinBlock_Lighting.java @@ -0,0 +1,28 @@ +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..3871eb16 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/MixinEBSID_EID.java @@ -0,0 +1,45 @@ +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 { + + @Shadow(remap = false) + @Dynamic + public abstract int eid$getID(int x, int y, int z); + + @Shadow(remap = false) + @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..4764c3ab --- /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/MixinWorld.java b/src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/MixinWorld.java index 01d5cb94..57fe8eb4 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/ext/EBSIDAccessor.java b/src/main/java/com/cardinalstar/cubicchunks/mixin/ext/EBSIDAccessor.java new file mode 100644 index 00000000..67d45521 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/mixin/ext/EBSIDAccessor.java @@ -0,0 +1,8 @@ +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/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/PacketEncoderCube.java b/src/main/java/com/cardinalstar/cubicchunks/network/PacketEncoderCube.java index ea4677c9..2fef1f59 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/network/PacketEncoderCube.java +++ b/src/main/java/com/cardinalstar/cubicchunks/network/PacketEncoderCube.java @@ -37,9 +37,9 @@ import com.cardinalstar.cubicchunks.util.CubeStatusVisualizer.CubeStatus; import com.cardinalstar.cubicchunks.world.cube.Cube; import com.github.bsideup.jabel.Desugar; - import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; +import net.jpountz.lz4.LZ4Factory; @ParametersAreNonnullByDefault public class PacketEncoderCube extends CCPacketEncoder { @@ -90,7 +90,8 @@ 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); } @@ -99,11 +100,14 @@ 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 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..142aca41 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/network/PacketEncoderUpdateVisualizedBoxes.java @@ -0,0 +1,74 @@ +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..0f5d2ab3 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; @@ -65,8 +71,8 @@ import com.cardinalstar.cubicchunks.world.savedata.WorldFormatSavedData; import com.google.common.collect.ListMultimap; 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 +111,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 +315,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 +322,35 @@ 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 +376,7 @@ private void doEagerLoading() { cubeIter.remove(); request.completed = true; + CubeStatusVisualizer.remove(request.pos, CubeStatus.Enqueued); processed++; @@ -420,6 +451,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 +591,7 @@ public EagerCubeLoadRequest(CubePos pos, Requirement effort) { public void cancel() { this.cancelled = true; + CubeStatusVisualizer.remove(pos, CubeStatus.Enqueued); } @Override @@ -560,6 +646,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..34cb5b9d 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/server/WorldSyncStateMachine.java +++ b/src/main/java/com/cardinalstar/cubicchunks/server/WorldSyncStateMachine.java @@ -167,6 +167,10 @@ 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..282537ed 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,110 @@ 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..fcdff496 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/server/chunkio/CubeLoaderServer.java +++ b/src/main/java/com/cardinalstar/cubicchunks/server/chunkio/CubeLoaderServer.java @@ -457,6 +457,57 @@ 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) { @@ -464,7 +515,10 @@ private void handleSideEffects(GenerationResult result, boolean doColumns, bo if (info != null && info.column != null) { CubicChunks.LOGGER - .warn("Worldgen side-effect replaced column at {},{}!", column.xPosition, column.zPosition); + .warn("Worldgen side-effect tried to replace column at {},{}! (in-memory source: {}, attempted replacement source: {})", + column.xPosition, column.zPosition, + info.source, + ObjectSource.GeneratedSideEffect); continue; } @@ -486,7 +540,10 @@ private void handleSideEffects(GenerationResult result, boolean doColumns, bo if (info != null && info.cube != null) { CubicChunks.LOGGER - .warn("Worldgen side-effect replaced cube at {},{},{}!", cube.getX(), cube.getY(), cube.getZ()); + .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 +567,7 @@ private enum ObjectSource { None, Disk, Generated, + Added, GeneratedSideEffect, Boundary } @@ -713,10 +771,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 +783,8 @@ private void loadBoundaryCube() { } this.cube = new BoundaryCube(this.column.column, this.getY()); + this.source = ObjectSource.Boundary; + onCubeLoaded(); } @@ -746,10 +807,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 +895,10 @@ 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..25bc6da4 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,19 @@ 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<>(); @@ -43,59 +57,52 @@ public static void sync(ServerTickEvent event) { if (!CubicChunksConfig.enableChunkStatusDebugging) { if (wasSent) { wasSent = false; - for (EntityPlayerMP player : MinecraftServer.getServer() - .getConfigurationManager().playerEntityList) { - // BoxVisualizer.sendBoxes(player, Duration.ofMinutes(0), new ArrayList<>(), true); + for (EntityPlayerMP player : MinecraftServer.getServer().getConfigurationManager().playerEntityList) { + 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); + for (EntityPlayerMP player : MinecraftServer.getServer().getConfigurationManager().playerEntityList) { + 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/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..a141406e --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/util/JavaUtils.java @@ -0,0 +1,24 @@ +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..dcc2bd70 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/util/boxvisualizer/BoxVisualizer.java @@ -0,0 +1,29 @@ +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..f747aff7 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/util/boxvisualizer/QuadSorter.java @@ -0,0 +1,121 @@ +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..5e1950ac --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/util/boxvisualizer/VisualizedBox.java @@ -0,0 +1,34 @@ +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..0e6e9798 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/util/boxvisualizer/VisualizedBoxRenderer.java @@ -0,0 +1,172 @@ +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..61fcc18e --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/world/layer/LayeredWorld.java @@ -0,0 +1,136 @@ +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..fabbe9d5 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/world/layer/WorldLayer.java @@ -0,0 +1,54 @@ +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..4bee9984 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/NormalizedSampler.java @@ -0,0 +1,76 @@ +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..6dc14275 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,53 @@ 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..0fed4da5 --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/ScaledSampler.java @@ -0,0 +1,44 @@ +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/SimplexNoiseSampler.java deleted file mode 100644 index ea0e87f9..00000000 --- a/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/SimplexNoiseSampler.java +++ /dev/null @@ -1,185 +0,0 @@ -package com.cardinalstar.cubicchunks.world.worldgen.noise; - -import java.util.Random; - -import net.minecraft.util.MathHelper; - -public class SimplexNoiseSampler 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 } }; - private static final double SQRT_3 = Math.sqrt(3.0D); - private static final double SKEW_FACTOR_2D; - private static final double UNSKEW_FACTOR_2D; - private final int[] permutations = new int[512]; - public final double originX; - public final double originY; - public final double originZ; - - public SimplexNoiseSampler(Random random) { - this.originX = random.nextDouble() * 256.0D; - this.originY = random.nextDouble() * 256.0D; - this.originZ = random.nextDouble() * 256.0D; - - for (int i = 0; i < 256; i++) { - this.permutations[i] = i; - } - - for (int i = 0; i < 256; ++i) { - int k = random.nextInt(256 - i); - int l = this.permutations[i]; - this.permutations[i] = this.permutations[k + i]; - this.permutations[k + i] = l; - } - } - - private int getGradient(int hash) { - return this.permutations[hash & 255]; - } - - protected static double dot(int[] gArr, double x, double y, double z) { - return (double) gArr[0] * x + (double) gArr[1] * y + (double) gArr[2] * z; - } - - private double grad(int hash, double x, double y, double z, double distance) { - double d = distance - x * x - y * y - z * z; - double f; - if (d < 0.0D) { - f = 0.0D; - } else { - d *= d; - f = d * d * dot(GRADIENTS[hash], x, y, z); - } - - return f; - } - - @Override - public double sample(double x, double y) { - double d = (x + y) * SKEW_FACTOR_2D; - int i = MathHelper.floor_double(x + d); - int j = MathHelper.floor_double(y + d); - double e = (double) (i + j) * UNSKEW_FACTOR_2D; - double f = (double) i - e; - 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; - } - - double p = h - (double) n + UNSKEW_FACTOR_2D; - double q = k - (double) o + UNSKEW_FACTOR_2D; - double r = h - 1.0D + 2.0D * UNSKEW_FACTOR_2D; - double s = k - 1.0D + 2.0D * UNSKEW_FACTOR_2D; - int t = i & 255; - int u = j & 255; - int v = this.getGradient(t + this.getGradient(u)) % 12; - int w = this.getGradient(t + n + this.getGradient(u + o)) % 12; - int z = this.getGradient(t + 1 + this.getGradient(u + 1)) % 12; - double aa = this.grad(v, h, k, 0.0D, 0.5D); - double ab = this.grad(w, p, q, 0.0D, 0.5D); - double ac = this.grad(z, r, s, 0.0D, 0.5D); - return 70.0D * (aa + ab + ac); - } - - @Override - public double sample(double x, double y, double z) { - double e = (x + y + z) * 0.3333333333333333D; - int i = MathHelper.floor_double(x + e); - int j = MathHelper.floor_double(y + e); - int k = MathHelper.floor_double(z + e); - double g = (double) (i + j + k) * 0.16666666666666666D; - double h = (double) i - g; - double l = (double) j - g; - double m = (double) k - g; - double n = x - h; - double o = y - l; - double p = z - m; - byte w; - byte aa; - byte ab; - byte ac; - byte ad; - byte bc; - if (n >= o) { - if (o >= p) { - w = 1; - aa = 0; - ab = 0; - ac = 1; - ad = 1; - bc = 0; - } else if (n >= p) { - w = 1; - aa = 0; - ab = 0; - ac = 1; - ad = 0; - bc = 1; - } else { - w = 0; - aa = 0; - ab = 1; - ac = 1; - ad = 0; - bc = 1; - } - } else if (o < p) { - w = 0; - aa = 0; - ab = 1; - ac = 0; - ad = 1; - bc = 1; - } else if (n < p) { - w = 0; - aa = 1; - ab = 0; - ac = 0; - ad = 1; - bc = 1; - } else { - w = 0; - aa = 1; - ab = 0; - ac = 1; - ad = 1; - bc = 0; - } - - double bd = n - (double) w + 0.16666666666666666D; - double be = o - (double) aa + 0.16666666666666666D; - double bf = p - (double) ab + 0.16666666666666666D; - double bg = n - (double) ac + 0.3333333333333333D; - double bh = o - (double) ad + 0.3333333333333333D; - double bi = p - (double) bc + 0.3333333333333333D; - double bj = n - 1.0D + 0.5D; - double bk = o - 1.0D + 0.5D; - double bl = p - 1.0D + 0.5D; - int bm = i & 255; - int bn = j & 255; - int bo = k & 255; - int bp = this.getGradient(bm + this.getGradient(bn + this.getGradient(bo))) % 12; - int bq = this.getGradient(bm + w + this.getGradient(bn + aa + this.getGradient(bo + ab))) % 12; - int br = this.getGradient(bm + ac + this.getGradient(bn + ad + this.getGradient(bo + bc))) % 12; - int bs = this.getGradient(bm + 1 + this.getGradient(bn + 1 + this.getGradient(bo + 1))) % 12; - double bt = this.grad(bp, n, o, p, 0.6D); - double bu = this.grad(bq, bd, be, bf, 0.6D); - double bv = this.grad(br, bg, bh, bi, 0.6D); - double bw = this.grad(bs, bj, bk, bl, 0.6D); - return 32.0D * (bt + bu + bv + bw); - } - - static { - SKEW_FACTOR_2D = 0.5D * (SQRT_3 - 1.0D); - UNSKEW_FACTOR_2D = (3.0D - SQRT_3) / 6.0D; - } -} diff --git a/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/SimplexSampler.java b/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/SimplexSampler.java new file mode 100644 index 00000000..b097b8aa --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/SimplexSampler.java @@ -0,0 +1,306 @@ +package com.cardinalstar.cubicchunks.world.worldgen.noise; + +import java.nio.ByteBuffer; +import java.util.Random; + +import net.minecraft.util.MathHelper; + +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelBuilder; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferDataType; + +/// 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 } }; + private static final double SQRT_3 = Math.sqrt(3.0D); + private static final double SKEW_FACTOR_2D; + private static final double UNSKEW_FACTOR_2D; + private final int[] permutations = new int[512]; + public final double originX; + public final double originY; + public final double originZ; + + public SimplexSampler(Random random) { + this.originX = random.nextDouble() * 256.0D; + this.originY = random.nextDouble() * 256.0D; + this.originZ = random.nextDouble() * 256.0D; + + for (int i = 0; i < 256; i++) { + this.permutations[i] = i; + } + + for (int i = 0; i < 256; ++i) { + int k = random.nextInt(256 - i); + int l = this.permutations[i]; + this.permutations[i] = this.permutations[k + i]; + this.permutations[k + i] = l; + } + } + + private int getGradient(int hash) { + return this.permutations[hash & 255]; + } + + protected static double dot(int[] gArr, double x, double y, double z) { + return (double) gArr[0] * x + (double) gArr[1] * y + (double) gArr[2] * z; + } + + private double grad(int hash, double x, double y, double z, double distance) { + double d = distance - x * x - y * y - z * z; + double f; + if (d < 0.0D) { + f = 0.0D; + } else { + d *= d; + f = d * d * dot(GRADIENTS[hash], x, y, z); + } + + return f; + } + + @Override + public double sample(double x, double y) { + double d = (x + y) * SKEW_FACTOR_2D; + int i = MathHelper.floor_double(x + d); + int j = MathHelper.floor_double(y + d); + double e = (double) (i + j) * UNSKEW_FACTOR_2D; + double f = (double) i - e; + 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; + } + + double p = h - (double) n + UNSKEW_FACTOR_2D; + double q = k - (double) o + UNSKEW_FACTOR_2D; + double r = h - 1.0D + 2.0D * UNSKEW_FACTOR_2D; + double s = k - 1.0D + 2.0D * UNSKEW_FACTOR_2D; + int t = i & 255; + int u = j & 255; + int v = this.getGradient(t + this.getGradient(u)) % 12; + int w = this.getGradient(t + n + this.getGradient(u + o)) % 12; + int z = this.getGradient(t + 1 + this.getGradient(u + 1)) % 12; + double aa = this.grad(v, h, k, 0.0D, 0.5D); + double ab = this.grad(w, p, q, 0.0D, 0.5D); + double ac = this.grad(z, r, s, 0.0D, 0.5D); + return 70.0D * (aa + ab + ac); + } + + @Override + public double sample(double x, double y, double z) { + double e = (x + y + z) * 0.3333333333333333D; + int i = MathHelper.floor_double(x + e); + int j = MathHelper.floor_double(y + e); + int k = MathHelper.floor_double(z + e); + double g = (double) (i + j + k) * 0.16666666666666666D; + double h = (double) i - g; + double l = (double) j - g; + double m = (double) k - g; + double n = x - h; + double o = y - l; + double p = z - m; + byte w; + byte aa; + byte ab; + byte ac; + byte ad; + byte bc; + if (n >= o) { + if (o >= p) { + w = 1; + aa = 0; + ab = 0; + ac = 1; + ad = 1; + bc = 0; + } else if (n >= p) { + w = 1; + aa = 0; + ab = 0; + ac = 1; + ad = 0; + bc = 1; + } else { + w = 0; + aa = 0; + ab = 1; + ac = 1; + ad = 0; + bc = 1; + } + } else if (o < p) { + w = 0; + aa = 0; + ab = 1; + ac = 0; + ad = 1; + bc = 1; + } else if (n < p) { + w = 0; + aa = 1; + ab = 0; + ac = 0; + ad = 1; + bc = 1; + } else { + w = 0; + aa = 1; + ab = 0; + ac = 1; + ad = 1; + bc = 0; + } + + double bd = n - (double) w + 0.16666666666666666D; + double be = o - (double) aa + 0.16666666666666666D; + double bf = p - (double) ab + 0.16666666666666666D; + double bg = n - (double) ac + 0.3333333333333333D; + double bh = o - (double) ad + 0.3333333333333333D; + double bi = p - (double) bc + 0.3333333333333333D; + double bj = n - 1.0D + 0.5D; + double bk = o - 1.0D + 0.5D; + double bl = p - 1.0D + 0.5D; + int bm = i & 255; + int bn = j & 255; + int bo = k & 255; + int bp = this.getGradient(bm + this.getGradient(bn + this.getGradient(bo))) % 12; + int bq = this.getGradient(bm + w + this.getGradient(bn + aa + this.getGradient(bo + ab))) % 12; + int br = this.getGradient(bm + ac + this.getGradient(bn + ad + this.getGradient(bo + bc))) % 12; + int bs = this.getGradient(bm + 1 + this.getGradient(bn + 1 + this.getGradient(bo + 1))) % 12; + double bt = this.grad(bp, n, o, p, 0.6D); + double bu = this.grad(bq, bd, be, bf, 0.6D); + double bv = this.grad(br, bg, bh, bi, 0.6D); + double bw = this.grad(bs, bj, bk, bl, 0.6D); + return 32.0D * (bt + bu + bv + bw); + } + + static { + 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"); + + ByteBuffer permBytes = ByteBuffer.allocateDirect(permutations.length * 4); + permBytes.asIntBuffer().put(permutations); + + var perms = builder.constants.append(BufferDataType.i32, permBytes); + + float skew = (float) SKEW_FACTOR_2D; + float unskew = (float) UNSKEW_FACTOR_2D; + + // Gradients: GRADIENTS[][0] and [][1] for indices 0-11 + builder.preamble + .append("float ").append(funcName).append("(float px, float py) {\n") + .append(" const int GX[12] = int[12](1,-1,1,-1,1,-1,1,-1,0,0,0,0);\n") + .append(" const int GY[12] = int[12](1,1,-1,-1,0,0,0,0,1,-1,1,-1);\n") + .append(" float d = (px + py) * ").append(skew).append("f;\n") + .append(" int i = int(floor(px + d));\n") + .append(" int j = int(floor(py + d));\n") + .append(" float e = float(i + j) * ").append(unskew).append("f;\n") + .append(" float h = px - (float(i) - e);\n") + .append(" float k = py - (float(j) - e);\n") + .append(" int n = h > k ? 1 : 0;\n") + .append(" int o = h > k ? 0 : 1;\n") + .append(" float p = h - float(n) + ").append(unskew).append("f;\n") + .append(" float q = k - float(o) + ").append(unskew).append("f;\n") + .append(" float r = h - 1.0f + 2.0f * ").append(unskew).append("f;\n") + .append(" float s = k - 1.0f + 2.0f * ").append(unskew).append("f;\n") + .append(" int t = i & 255;\n") + .append(" int u = j & 255;\n") + .append(" int v = ").append(perms.access("(t + " + perms.access("u") + ") & 255")).append(" % 12;\n") + .append(" int w = ").append(perms.access("(t + n + " + perms.access("(u + o) & 255") + ") & 255")).append(" % 12;\n") + .append(" int z = ").append(perms.access("(t + 1 + " + perms.access("(u + 1) & 255") + ") & 255")).append(" % 12;\n") + .append(" float t0 = max(0.0f, 0.5f - h*h - k*k);\n") + .append(" float n0 = t0*t0*t0*t0 * (float(GX[v])*h + float(GY[v])*k);\n") + .append(" float t1 = max(0.0f, 0.5f - p*p - q*q);\n") + .append(" float n1 = t1*t1*t1*t1 * (float(GX[w])*p + float(GY[w])*q);\n") + .append(" float t2 = max(0.0f, 0.5f - r*r - s*s);\n") + .append(" float n2 = t2*t2*t2*t2 * (float(GX[z])*r + float(GY[z])*s);\n") + .append(" return 70.0f * (n0 + n1 + n2);\n") + .append("}\n"); + + 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) { + String funcName = builder.createName("simplex3d"); + + ByteBuffer permBytes = ByteBuffer.allocateDirect(permutations.length * 4); + permBytes.asIntBuffer().put(permutations); + + var perms = builder.constants.append(BufferDataType.i32, permBytes); + + // Gradients: GRADIENTS[][0..2] for indices 0-11 + builder.preamble + .append("float ").append(funcName).append("(float px, float py, float pz) {\n") + .append(" const int GX[12] = int[12](1,-1,1,-1,1,-1,1,-1,0,0,0,0);\n") + .append(" const int GY[12] = int[12](1,1,-1,-1,0,0,0,0,1,-1,1,-1);\n") + .append(" const int GZ[12] = int[12](0,0,0,0,1,1,-1,-1,1,1,-1,-1);\n") + .append(" float sk = (px + py + pz) * 0.3333333333f;\n") + .append(" int i = int(floor(px + sk));\n") + .append(" int j = int(floor(py + sk));\n") + .append(" int k = int(floor(pz + sk));\n") + .append(" float g = float(i + j + k) * 0.1666666667f;\n") + .append(" float x0 = px - (float(i) - g);\n") + .append(" float y0 = py - (float(j) - g);\n") + .append(" float z0 = pz - (float(k) - g);\n") + // 6-way vertex ordering (mirrors the Java if/else chain exactly) + .append(" int i1, j1, k1, i2, j2, k2;\n") + .append(" if (x0 >= y0) {\n") + .append(" if (y0 >= z0) { i1=1;j1=0;k1=0; i2=1;j2=1;k2=0; }\n") + .append(" else if (x0 >= z0) { i1=1;j1=0;k1=0; i2=1;j2=0;k2=1; }\n") + .append(" else { i1=0;j1=0;k1=1; i2=1;j2=0;k2=1; }\n") + .append(" } else {\n") + .append(" if (y0 < z0) { i1=0;j1=0;k1=1; i2=0;j2=1;k2=1; }\n") + .append(" else if (x0 < z0) { i1=0;j1=1;k1=0; i2=0;j2=1;k2=1; }\n") + .append(" else { i1=0;j1=1;k1=0; i2=1;j2=1;k2=0; }\n") + .append(" }\n") + .append(" float x1 = x0 - float(i1) + 0.1666666667f;\n") + .append(" float y1 = y0 - float(j1) + 0.1666666667f;\n") + .append(" float z1 = z0 - float(k1) + 0.1666666667f;\n") + .append(" float x2 = x0 - float(i2) + 0.3333333333f;\n") + .append(" float y2 = y0 - float(j2) + 0.3333333333f;\n") + .append(" float z2 = z0 - float(k2) + 0.3333333333f;\n") + .append(" float x3 = x0 - 0.5f;\n") + .append(" float y3 = y0 - 0.5f;\n") + .append(" float z3 = z0 - 0.5f;\n") + .append(" int ii = i & 255;\n") + .append(" int jj = j & 255;\n") + .append(" int kk = k & 255;\n") + .append(" int g0 = ").append(perms.access("(ii + " + perms.access("(jj + " + perms.access("kk") + ") & 255") + ") & 255")).append(" % 12;\n") + .append(" int g1 = ").append(perms.access("(ii + i1 + " + perms.access("(jj + j1 + " + perms.access("(kk + k1) & 255") + ") & 255") + ") & 255")).append(" % 12;\n") + .append(" int g2 = ").append(perms.access("(ii + i2 + " + perms.access("(jj + j2 + " + perms.access("(kk + k2) & 255") + ") & 255") + ") & 255")).append(" % 12;\n") + .append(" int g3 = ").append(perms.access("(ii + 1 + " + perms.access("(jj + 1 + " + perms.access("(kk + 1) & 255") + ") & 255") + ") & 255")).append(" % 12;\n") + .append(" float d0 = 0.6f - x0*x0 - y0*y0 - z0*z0;\n") + .append(" float n0 = (d0 < 0.0f) ? 0.0f : d0*d0*d0*d0 * (float(GX[g0])*x0 + float(GY[g0])*y0 + float(GZ[g0])*z0);\n") + .append(" float d1 = 0.6f - x1*x1 - y1*y1 - z1*z1;\n") + .append(" float n1 = (d1 < 0.0f) ? 0.0f : d1*d1*d1*d1 * (float(GX[g1])*x1 + float(GY[g1])*y1 + float(GZ[g1])*z1);\n") + .append(" float d2 = 0.6f - x2*x2 - y2*y2 - z2*z2;\n") + .append(" float n2 = (d2 < 0.0f) ? 0.0f : d2*d2*d2*d2 * (float(GX[g2])*x2 + float(GY[g2])*y2 + float(GZ[g2])*z2);\n") + .append(" float d3 = 0.6f - x3*x3 - y3*y3 - z3*z3;\n") + .append(" float n3 = (d3 < 0.0f) ? 0.0f : d3*d3*d3*d3 * (float(GX[g3])*x3 + float(GY[g3])*y3 + float(GZ[g3])*z3);\n") + .append(" return 32.0f * (n0 + n1 + n2 + n3);\n") + .append("}\n"); + + String result = builder.createName("simplex"); + builder.logic.append(" float ").append(result).append(" = ") + .append(funcName).append("(").append(x).append(", ").append(y).append(", ").append(z).append(");\n"); + return result; + } +} diff --git a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/CCEnhancedWorldGenerator.java b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/CCEnhancedWorldGenerator.java index 86e786b9..4597d296 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/CCEnhancedWorldGenerator.java +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/CCEnhancedWorldGenerator.java @@ -1,31 +1,75 @@ package com.cardinalstar.cubicchunks.worldgen.ccenhanced; +import static org.lwjgl.opengl.GL20.glUseProgram; +import static org.lwjgl.opengl.GL43.glDispatchCompute; + +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; +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 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.KernelContext; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelExecutor; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelSubmission; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelSubmissionResult; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelSubmissionToken; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.Noise2DKernelExecutor; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferAllocator; +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.GPUBuffer; +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 { +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; @@ -34,6 +78,7 @@ public class CCEnhancedWorldGenerator implements IWorldGenerator { 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. @@ -45,7 +90,8 @@ public CCEnhancedWorldGenerator(World world) { this.climateSystem = new ClimateSystem(seed); this.biomeCache = new CCBiomeCache(climateSystem); this.terrainGen = new CCTerrainGenerator(seed); - this.contextCache = new LinkedHashMap(CONTEXT_CACHE_CAPACITY, 0.75f, true) { + this.surfacePainter = new CCSurfacePainter(); + this.contextCache = new LinkedHashMap<>(CONTEXT_CACHE_CAPACITY, 0.75f, true) { @Override protected boolean removeEldestEntry(Map.Entry eldest) { @@ -54,6 +100,14 @@ protected boolean removeEldestEntry(Map.Entry eldest) { }; } + private @NotNull ICubeLoader getCubeLoader() { + return getCubeProvider().getCubeLoader(); + } + + private CubeProviderServer getCubeProvider() { + return ((Server) this.world).getCubeCache(); + } + // ------------------------------------------------------------------------- // Column // ------------------------------------------------------------------------- @@ -61,14 +115,22 @@ protected boolean removeEldestEntry(Map.Entry eldest) { @Override public GenerationResult provideColumn(World world, int columnX, int columnZ) { Chunk chunk = new Chunk(world, columnX, columnZ); - ((IColumnInternal) chunk).setColumn(true); - ColumnContext ctx = terrainGen.computeColumnContext(chunk, columnX, columnZ, biomeCache); - contextCache.put(packKey(columnX, columnZ), ctx); + 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, terrainGen.buildEbs(world, ctx, cubeY))); + cubes.add(new Cube(chunk, cubeY, ebsArr[cubeY])); } return new GenerationResult<>(chunk, null, cubes); @@ -92,8 +154,19 @@ public GenerationResult provideCube(@Nullable Chunk chunk, int cubeX, int 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++) { - generatedCubes.add(new Cube(chunk, cy, terrainGen.buildEbs(world, ctx, 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])); } } @@ -162,7 +235,269 @@ private ColumnContext getOrComputeContext(Chunk chunk, int chunkX, int chunkZ) { 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(); + KernelContext.getScheduler().runAndWait(() -> { + 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(); + }); + } + + 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, null, ImmutableMap.of("temperature", temperature, "humidity", humidity, "continentalness", continentalness, "erosion", erosion)); + + var lookup = plan.submit(lookupKernel, null, ImmutableMap.of("distances", distances.get("distances"))); + + // 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, null, ImmutableMap.of( + "distances", distances.get("distances"), + "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("heightMap", heightData.get("heightMap"))); + + 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("blocks").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 y = 0; y < 16; y++) { + for (int z = 0; z < 16; z++) { + 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 implements KernelExecutor { + + private final int program; + + public BlockGenKernelExecutor() { + String code = """ + #version 430 core + + layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in; + + $uniform + + layout(std430, binding = 0) buffer Arena { uint arena[]; }; + layout(std430, binding = 1) readonly buffer Uniforms { BlockPickerUniform uniforms[]; }; + + void main() { + uint id = gl_WorkGroupID.x; + + uint lx = gl_LocalInvocationID.x; + uint ly = gl_LocalInvocationID.y; + uint lz = gl_LocalInvocationID.z + gl_WorkGroupID.z; + + int cubeY = uniforms[id].cubeY; + uint heightmapOffset = uniforms[id].heightmapOffset; + uint blockOffset = uniforms[id].blockOffset; + + int gy = int(ly) + (cubeY << 4); + + int topBlock = int(uintBitsToFloat(arena[heightmapOffset + ((lz << 4) | lx)])); + + 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 + + arena[blockOffset + ((lz << 8) | (ly << 4) | lx)] = block; + } + """ + .replace("$uniform", BlockPickerUniformGLStruct.SOURCE); + + this.program = KernelExecutor.createProgram(code); + } + + @Override + public Map getOutputs( + ComputePlan plan, + KernelSubmissionToken submission, + CubePos cubePos, + Map inputs + ) { + var heightMap = inputs.get("heightMap"); + Objects.requireNonNull(heightMap, "expected heightMap input buffer"); + heightMap.assertLayout(BufferDataType.f32, 16, 16); + + return ImmutableMap.of("blocks", plan.describeBuffer(submission, BufferDataType.u32, 16, 16, 16)); + } + + @Override + public KernelSubmissionResult[] submit(BufferAllocator alloc, KernelSubmission[] submissions) { + glUseProgram(this.program); + + BlockPickerUniformPrimitiveBuffer uniforms = new BlockPickerUniformPrimitiveBuffer(submissions.length); + + KernelSubmissionResult[] results = new KernelSubmissionResult[submissions.length]; + + uniforms.forEachFast((i, view) -> { + var key = submissions[i].key(); + + GPUBuffer input = submissions[i].inputs().get("heightMap"); + + GPUBuffer output = alloc.alloc(BufferDataType.u32, 16, 16, 16); + results[i] = new KernelSubmissionResult(ImmutableMap.of("blocks", output)); + + view.setCubeY(key.getY()); + view.setHeightmapOffset(input.getBufferOffset() / 4); + view.setBlockOffset(output.getBufferOffset() / 4); + + return true; + }); + + GPUBuffer uniformGPU = alloc.uniform(uniforms); + + alloc.bindSSBO(0); + uniformGPU.bind(1); + + glDispatchCompute(submissions.length, 1, 16); + + alloc.unbindSSBO(0); + uniformGPU.unbind(1); + + glUseProgram(0); + + return results; + } + } } 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..793d2b1c --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/BiomeDistanceKernelExecutor.java @@ -0,0 +1,131 @@ +package com.cardinalstar.cubicchunks.worldgen.ccenhanced.biome; + +import static org.lwjgl.opengl.GL20.glUseProgram; +import static org.lwjgl.opengl.GL43.glDispatchCompute; + +import java.util.Map; + +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.ComputePlan; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelConstantBuilder; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelExecutor; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelSubmission; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelSubmissionResult; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelSubmissionToken; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferAllocator; +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.GPUBuffer; +import com.google.common.collect.ImmutableMap; + +public class BiomeDistanceKernelExecutor implements KernelExecutor { + + @org.jetbrains.annotations.NotNull + private final CCBiomeGenBase[] biomes; + + private final int program; + + public BiomeDistanceKernelExecutor(CCBiomeGenBase[] biomes) { + this.biomes = biomes; + + KernelConstantBuilder biomeCoords = new KernelConstantBuilder("float", "BIOME_COORDS"); + + for (int i = 0; i < biomes.length; i++) { + biomeCoords.annotate(biomes[i].biomeName); + biomeCoords.add(biomes[i].climatePoint); + } + + String code = + """ + #version 430 core + + layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in; + + $uniform + + layout(std430, binding = 0) buffer Arena { float arena[]; }; + layout(std430, binding = 1) readonly buffer Uniforms { BiomeDistanceUniform uniforms[]; }; + + $biomeCoords + + void main() { + uint id = gl_WorkGroupID.x; + + uint x = gl_LocalInvocationID.x; + uint y = gl_LocalInvocationID.y; + uint biome = gl_LocalInvocationID.z + gl_WorkGroupID.z; + + uint pixelIndex = y * 16u + x; + uint biomeIndex = biome * 4u; + + uint temperatureOffset = uniforms[id].temperatureOffset; + uint humidityOffset = uniforms[id].humidityOffset; + uint continentalnessOffset = uniforms[id].continentalnessOffset; + uint erosionOffset = uniforms[id].erosionOffset; + uint distanceOffset = uniforms[id].distanceOffset; + + float a = arena[pixelIndex + temperatureOffset] - BIOME_COORDS[biomeIndex + 0u]; + float b = arena[pixelIndex + humidityOffset] - BIOME_COORDS[biomeIndex + 1u]; + float c = arena[pixelIndex + continentalnessOffset] - BIOME_COORDS[biomeIndex + 2u]; + float d = arena[pixelIndex + erosionOffset] - BIOME_COORDS[biomeIndex + 3u]; + + arena[biome * 256u + pixelIndex + distanceOffset] = sqrt(a * a + b * b + c * c + d * d); + } + """ + .replace("$biomeCount", Integer.toString(biomes.length)) + .replace("$uniform", BiomeDistanceUniformGLStruct.SOURCE) + .replace("$biomeCoords", biomeCoords.toString()); + + this.program = KernelExecutor.createProgram(code); + } + + @Override + public Map getOutputs( + ComputePlan plan, KernelSubmissionToken submission, Void key, + Map inputs + ) { + inputs.get("temperature").assertLayout(BufferDataType.f32, 16, 16); + inputs.get("humidity").assertLayout(BufferDataType.f32, 16, 16); + inputs.get("continentalness").assertLayout(BufferDataType.f32, 16, 16); + inputs.get("erosion").assertLayout(BufferDataType.f32, 16, 16); + + return ImmutableMap.of("distances", plan.describeBuffer(submission, BufferDataType.f32, 16, 16, biomes.length)); + } + + @Override + public KernelSubmissionResult[] submit(BufferAllocator alloc, KernelSubmission[] submissions) { + glUseProgram(this.program); + + BiomeDistanceUniformPrimitiveBuffer uniforms = new BiomeDistanceUniformPrimitiveBuffer(submissions.length); + + KernelSubmissionResult[] results = new KernelSubmissionResult[submissions.length]; + + uniforms.forEachFast((i, view) -> { + GPUBuffer output = alloc.alloc(BufferDataType.f32, 16, 16, biomes.length); + results[i] = new KernelSubmissionResult(ImmutableMap.of("distances", output)); + + Map inputs = submissions[i].inputs(); + + view.setTemperatureOffset(inputs.get("temperature").getBufferOffset() / 4); + view.setHumidityOffset(inputs.get("humidity").getBufferOffset() / 4); + view.setContinentalnessOffset(inputs.get("continentalness").getBufferOffset() / 4); + view.setErosionOffset(inputs.get("erosion").getBufferOffset() / 4); + view.setDistanceOffset(output.getBufferOffset() / 4); + + return true; + }); + + GPUBuffer uniformGPU = alloc.uniform(uniforms); + + alloc.bindSSBO(0); + uniformGPU.bind(1); + + glDispatchCompute(submissions.length, 1, biomes.length); + + alloc.unbindSSBO(0); + uniformGPU.unbind(1); + + glUseProgram(0); + + return results; + } +} 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..44f586bc --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/BiomeLookupKernelExecutor.java @@ -0,0 +1,142 @@ +package com.cardinalstar.cubicchunks.worldgen.ccenhanced.biome; + +import static org.lwjgl.opengl.GL20.glUseProgram; +import static org.lwjgl.opengl.GL43.glDispatchCompute; + +import java.util.Map; + +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.ComputePlan; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelExecutor; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelSubmission; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelSubmissionResult; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelSubmissionToken; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferAllocator; +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.GPUBuffer; +import com.google.common.collect.ImmutableMap; + +public class BiomeLookupKernelExecutor implements KernelExecutor { + + private final int program; + private final int biomeCount; + + public BiomeLookupKernelExecutor(int biomeCount) { + this.biomeCount = biomeCount; + + String code = + """ + #version 430 core + + layout(local_size_x = 16, local_size_z = 16) in; + + $uniform + + layout(std430, binding = 0) buffer Arena { uint arena[]; }; + layout(std430, binding = 1) readonly buffer Uniforms { BiomeLookupUniform uniforms[]; }; + + const uint BIOME_STRIDE = 16 * 16; + + void main() { + uint id = gl_WorkGroupID.x; + + uint x = gl_LocalInvocationID.x; + uint z = gl_LocalInvocationID.z; + + uint distanceOffset = uniforms[id].distanceOffset; + uint closestOffset = uniforms[id].closestOffset; + uint weightsOffset = uniforms[id].weightsOffset; + + uint columnOffset = z << 4 | 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 = 0; i < $biomeCount; i++) { + float value = uintBitsToFloat(arena[BIOME_STRIDE * i + columnOffset + distanceOffset]); + + 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 + arena[columnOffset * 4u + 0u + closestOffset] = uint(idx[0]); + arena[columnOffset * 4u + 1u + closestOffset] = uint(idx[1]); + arena[columnOffset * 4u + 2u + closestOffset] = uint(idx[2]); + arena[columnOffset * 4u + 3u + closestOffset] = 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 + arena[columnOffset * 4 + 0 + weightsOffset] = floatBitsToUint(invA * norm); + arena[columnOffset * 4 + 1 + weightsOffset] = floatBitsToUint(invB * norm); + arena[columnOffset * 4 + 2 + weightsOffset] = floatBitsToUint(invC * norm); + arena[columnOffset * 4 + 3 + weightsOffset] = floatBitsToUint(invD * norm); + } + """ + .replace("$uniform", BiomeLookupUniformGLStruct.SOURCE) + .replace("$biomeCount", Integer.toString(biomeCount)); + + this.program = KernelExecutor.createProgram(code); + } + + @Override + public Map getOutputs( + ComputePlan plan, KernelSubmissionToken submission, Void key, + Map inputs + ) { + inputs.get("distances").assertLayout(BufferDataType.f32, 16, 16, biomeCount); + + return ImmutableMap.of( + "closestBiomes", plan.describeBuffer(submission, BufferDataType.i32, 16, 16, 4), + "biomeWeights", plan.describeBuffer(submission, BufferDataType.f32, 16, 16, 4)); + } + + @Override + public KernelSubmissionResult[] submit(BufferAllocator alloc, KernelSubmission[] submissions) { + glUseProgram(this.program); + + BiomeLookupUniformPrimitiveBuffer uniforms = new BiomeLookupUniformPrimitiveBuffer(submissions.length); + + KernelSubmissionResult[] results = new KernelSubmissionResult[submissions.length]; + + uniforms.forEachFast((i, view) -> { + GPUBuffer distances = submissions[i].inputs().get("distances"); + GPUBuffer closestBiomes = alloc.alloc(BufferDataType.i32, 16, 16, 4); + GPUBuffer biomeWeights = alloc.alloc(BufferDataType.f32, 16, 16, 4); + + results[i] = new KernelSubmissionResult(ImmutableMap.of("closestBiomes", closestBiomes, "biomeWeights", biomeWeights)); + + view.setDistanceOffset(distances.getBufferOffset() / 4); + view.setClosestOffset(closestBiomes.getBufferOffset() / 4); + view.setWeightsOffset(biomeWeights.getBufferOffset() / 4); + + return true; + }); + + GPUBuffer uniformGPU = alloc.uniform(uniforms); + + alloc.bindSSBO(0); + uniformGPU.bind(1); + + glDispatchCompute(submissions.length, 1, 1); + + alloc.unbindSSBO(0); + uniformGPU.unbind(1); + + glUseProgram(0); + + return results; + } +} 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 index d644106f..e3c038a8 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/CCBiomeRegistry.java +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/CCBiomeRegistry.java @@ -20,6 +20,10 @@ 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); } @@ -81,37 +85,19 @@ static float distanceSq(float[] a, float[] b) { * sorted in ascending order (index 0 = smallest distance = primary biome). */ private static int[] topNSorted(float[] dists, int n) { - int[] indices = new int[n]; - float[] best = new float[n]; - Arrays.fill(best, Float.MAX_VALUE); - - // Selection: keep the n smallest distances - for (int i = 0; i < dists.length; i++) { - // Find the slot in our top-n that holds the worst (largest) distance - int worstSlot = 0; - for (int j = 1; j < n; j++) { - if (best[j] > best[worstSlot]) worstSlot = j; - } - if (dists[i] < best[worstSlot]) { - best[worstSlot] = dists[i]; - indices[worstSlot] = i; - } - } + int[] indices = new int[dists.length]; - // Insertion-sort the n results by distance (n is tiny — 3 by default) - for (int i = 1; i < n; i++) { - float key = best[i]; - int idx = indices[i]; - int j = i - 1; - while (j >= 0 && best[j] > key) { - best[j + 1] = best[j]; - indices[j + 1] = indices[j]; - j--; - } - best[j + 1] = key; - indices[j + 1] = idx; - } + 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 indices; + 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..e7150b9e --- /dev/null +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/HeightMapKernelExecutor.java @@ -0,0 +1,164 @@ +package com.cardinalstar.cubicchunks.worldgen.ccenhanced.biome; + +import static org.lwjgl.opengl.GL20.glUseProgram; +import static org.lwjgl.opengl.GL43.glDispatchCompute; + +import java.util.List; +import java.util.Map; + +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.ComputePlan; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelConstantBuilder; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelExecutor; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelSubmission; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelSubmissionResult; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelSubmissionToken; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferAllocator; +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.GPUBuffer; +import com.google.common.collect.ImmutableMap; + +/** + * 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 implements KernelExecutor { + + /** + * 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; + + private final int program; + private final int biomeCount; + + /** Embeds per-biome rootHeight and heightVariation as GLSL constant arrays. */ + public HeightMapKernelExecutor(List biomes) { + this.biomeCount = biomes.size(); + + KernelConstantBuilder biomeRoot = new KernelConstantBuilder("float", "BIOME_ROOT"); + KernelConstantBuilder biomeVar = new KernelConstantBuilder("float", "BIOME_VAR"); + + for (int i = 0; i < biomeCount; i++) { + CCBiomeGenBase b = biomes.get(i); + + biomeRoot.annotate(b.biomeName); + biomeRoot.add(b.rootHeight); + + biomeVar.annotate(b.biomeName); + biomeVar.add(b.heightVariation); + } + + String code = """ + #version 430 core + + layout(local_size_x = 16, local_size_z = 16) in; + + $uniform + + // distances[biome * 256 + columnOffset] — axis-major layout from BiomeDistanceKernelExecutor + layout(std430, binding = 0) buffer Arena { float arena[]; }; + layout(std430, binding = 1) readonly buffer Uniforms { HeightMapUniform uniforms[]; }; + + $biomeRoot + $biomeVar + + const float FALLOFF = $falloff; + + void main() { + uint id = gl_WorkGroupID.x; + + uint x = gl_LocalInvocationID.x; + uint z = gl_LocalInvocationID.z; + + uint distancesOffset = uniforms[id].distancesOffset; + uint hvNoiseOffset = uniforms[id].hvNoiseOffset; + uint heightMapOffset = uniforms[id].heightMapOffset; + + uint columnOffset = z << 4u | x; + + float blendedRoot = 0.0f; + float blendedVar = 0.0f; + float totalWeight = 0.0f; + + for (int i = 0; i < $biomeCount; i++) { + float dist = arena[uint(i) * 256u + columnOffset + distancesOffset]; + float w = exp(-FALLOFF * dist * dist); + blendedRoot += BIOME_ROOT[i] * w; + blendedVar += BIOME_VAR[i] * w; + totalWeight += w; + } + + blendedRoot /= totalWeight; + blendedVar /= totalWeight; + + arena[columnOffset + heightMapOffset] = blendedRoot + blendedVar * arena[columnOffset + hvNoiseOffset]; + } + """ + .replace("$uniform", HeightMapUniformGLStruct.SOURCE) + .replace("$biomeRoot", biomeRoot.toString()) + .replace("$biomeVar", biomeVar.toString()) + .replace("$biomeCount", Integer.toString(biomeCount)) + .replace("$falloff", FALLOFF + "f"); + + this.program = KernelExecutor.createProgram(code); + } + + @Override + public Map getOutputs( + ComputePlan plan, KernelSubmissionToken submission, Void key, + Map inputs + ) { + inputs.get("distances").assertLayout(BufferDataType.f32, 16, 16, biomeCount); + inputs.get("hvNoise").assertLayout(BufferDataType.f32, 16, 16); + + return ImmutableMap.of("heightMap", plan.describeBuffer(submission, BufferDataType.f32, 16, 16)); + } + + @Override + public KernelSubmissionResult[] submit(BufferAllocator alloc, KernelSubmission[] submissions) { + glUseProgram(this.program); + + HeightMapUniformPrimitiveBuffer uniforms = new HeightMapUniformPrimitiveBuffer(submissions.length); + + KernelSubmissionResult[] results = new KernelSubmissionResult[submissions.length]; + + uniforms.forEachFast((i, view) -> { + GPUBuffer distances = submissions[i].inputs().get("distances"); + GPUBuffer hvNoise = submissions[i].inputs().get("hvNoise"); + GPUBuffer heightMap = alloc.alloc(BufferDataType.i32, 16, 16); + + results[i] = new KernelSubmissionResult(ImmutableMap.of("heightMap", heightMap)); + + view.setDistancesOffset(distances.getBufferOffset() / 4); + view.setHvNoiseOffset(hvNoise.getBufferOffset() / 4); + view.setHeightMapOffset(heightMap.getBufferOffset() / 4); + + return true; + }); + + GPUBuffer uniformGPU = alloc.uniform(uniforms); + + alloc.bindSSBO(0); + uniformGPU.bind(1); + + glDispatchCompute(submissions.length, 1, 1); + + alloc.unbindSSBO(0); + uniformGPU.unbind(1); + + glUseProgram(0); + + return results; + } +} 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 index 1cc0e18d..6ef6e96f 100644 --- 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 @@ -4,7 +4,10 @@ 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. @@ -49,57 +52,65 @@ 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 }, -1.8f, 0.1f); + 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 }, -1.0f, 0.1f); + 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 }, -0.1f, 0.05f); + 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 }, 0.1f, 0.05f); + 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 }, 0.4f, 0.3f); + 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 }, 0.3f, 0.2f); + 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 }, 0.7f, 0.5f); + 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 }, 1.0f, 0.8f); + 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 }, 0.1f, 0.1f); + 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 }, 0.1f, 0.1f); + 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 }, 0.2f, 0.1f); + 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 }, -2.5f, 0.2f); + 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); + 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 index 53b52f65..911a937a 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/climate/ClimateAxis.java +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/climate/ClimateAxis.java @@ -1,5 +1,7 @@ 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]. @@ -11,12 +13,5 @@ public interface ClimateAxis { /** Sample the axis at world coordinates (x, z). Returns a value in [-1, 1]. */ double sample(double x, double z); - /** - * Default position on this axis for biomes that were registered before this axis existed - * or whose climate point is shorter than the current axis list. - * Most axes should return 0; some (e.g. a future "weirdness" axis) may prefer a different neutral. - */ - default float defaultValue() { - return 0.0f; - } + void compileShader(KernelBuilder builder, String functionName); } 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 index ba8c5273..77f8890a 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/climate/ClimateSystem.java +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/climate/ClimateSystem.java @@ -1,12 +1,30 @@ package com.cardinalstar.cubicchunks.worldgen.ccenhanced.climate; +import static org.lwjgl.opengl.GL20.glUseProgram; +import static org.lwjgl.opengl.GL43.glDispatchCompute; + import java.util.ArrayList; import java.util.List; -import java.util.Random; +import java.util.Map; + +import net.minecraft.world.ChunkCoordIntPair; + +import org.jetbrains.annotations.Nullable; -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.api.worldgen.hwaccel.ComputePlan; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelBuilder; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelExecutor; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelSubmission; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelSubmissionResult; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelSubmissionToken; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.Noise2DUniformGLStruct; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.Noise2DUniformPrimitiveBuffer; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferAllocator; +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.ConstantHardwareBuffer; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.GPUBuffer; +import com.google.common.collect.ImmutableMap; import com.gtnewhorizon.gtnhlib.hash.Fnv1a64; /** @@ -32,10 +50,8 @@ public class ClimateSystem { 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 static final double REMAP_EXPONENT = 0.65; - private final List axes; - private final NoiseSampler warpNoise; + private final ClimateAxis warp; public ClimateSystem(long worldSeed) { axes = new ArrayList<>(); @@ -50,41 +66,152 @@ public ClimateSystem(long worldSeed) { // Warp noise: 2 octaves, 1/800 frequency. Use axisIndex=-1 as seed slot. long warpSeed = Fnv1a64.hashStep(Fnv1a64.hashStep(Fnv1a64.initialState(), worldSeed), -1L); - warpNoise = new ScaledNoise(new OctavesSampler(new Random(warpSeed), 2), WARP_FREQ); + 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, REMAP_EXPONENT)); + 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 ClimatePoint sample(double x, double z) { - float[] values = new float[axes.size()]; + 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 = warpOffsetX(i); - double oz = warpOffsetZ(i); + double ox = WARP_OFFSET_X[i]; + double oz = WARP_OFFSET_Z[i]; // Sample warp displacement for this axis - double wx = warpNoise.sample(x + ox, z + oz); - double wz = warpNoise.sample(x + ox + 500.0, z + oz + 500.0); - values[i] = (float) axes.get(i) - .sample(x + wx * WARP_MAG, z + wz * WARP_MAG); + 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 new ClimatePoint(values); - } - public int numAxes() { - return axes.size(); + return pooled; } - private double warpOffsetX(int i) { - return i < WARP_OFFSET_X.length ? WARP_OFFSET_X[i] : i * 1000.0; + /** + * 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 double warpOffsetZ(int i) { - return i < WARP_OFFSET_Z.length ? WARP_OFFSET_Z[i] : i * 1000.0 + 500.0; + private class AxisSamplerKernelExecutor implements KernelExecutor { + + private final int axisIndex; + private final int program; + + private final ConstantHardwareBuffer constants; + + public AxisSamplerKernelExecutor(int axisIndex, ClimateAxis axis) { + this.axisIndex = axisIndex; + + KernelBuilder builder = new KernelBuilder(); + + ClimateSystem.this.warp.compileShader(builder, "warp"); + axis.compileShader(builder, axis.getName()); + + float warpOffsetX = (float) WARP_OFFSET_X[axisIndex]; + float warpOffsetZ = (float) WARP_OFFSET_Z[axisIndex]; + int axisBase = axisIndex * 256; + + String code = + """ + #version 430 core + + layout(local_size_x = 16, local_size_z = 16) in; + + $uniform + + layout(std430, binding = 0) writeonly buffer Arena { float arena[]; }; + layout(std430, binding = 1) readonly buffer Uniforms { Noise2DUniform uniforms[]; }; + layout(std430, binding = 2) readonly buffer Constants { uint constants[]; }; + + $preamble + + void main() { + uint id = gl_WorkGroupID.x; + + uint x = gl_LocalInvocationID.x; + uint z = gl_LocalInvocationID.z; + + int offsetX = uniforms[id].offsetX; + int offsetZ = uniforms[id].offsetZ; + uint outputOffset = uniforms[id].outputOffset; + + int gx = offsetX + int(x); + int gz = offsetZ + int(z); + + $logic + + float warpX = warp(gx + $warpOffsetX, gz + $warpOffsetZ) * 200.0f; + float warpZ = warp(gx + $warpOffsetX + 500.0f, gz + $warpOffsetZ + 500.0f) * 200.0f; + + arena[(z << 4) + x + outputOffset] = $axisFunc(gx + warpX, gz + warpZ); + } + """ + .replace("$uniform", Noise2DUniformGLStruct.SOURCE) + .replace("$preamble", builder.preamble.toString()) + .replace("$logic", builder.logic.toString()) + .replace("$warpOffsetX", warpOffsetX + "f") + .replace("$warpOffsetZ", warpOffsetZ + "f") + .replace("$axisBase", Integer.toString(axisBase)) + .replace("$axisFunc", axis.getName()); + + this.program = KernelExecutor.createProgram(code); + this.constants = builder.constants.finish(); + } + + @Override + public Map getOutputs( + ComputePlan plan, KernelSubmissionToken submission, ChunkCoordIntPair key, + Map inputs + ) { + return ImmutableMap.of("noise", plan.describeBuffer(submission, BufferDataType.f32, 16, 16)); + } + + @Override + public KernelSubmissionResult[] submit(BufferAllocator alloc, KernelSubmission[] submissions) { + glUseProgram(this.program); + + Noise2DUniformPrimitiveBuffer uniforms = new Noise2DUniformPrimitiveBuffer(submissions.length); + + KernelSubmissionResult[] results = new KernelSubmissionResult[submissions.length]; + + uniforms.forEachFast((i, view) -> { + var key = submissions[i].key(); + + GPUBuffer output = alloc.alloc(BufferDataType.f32, 16, 16); + results[i] = new KernelSubmissionResult(ImmutableMap.of("noise", output)); + + view.setOffsetX(key.chunkXPos << 4); + view.setOffsetZ(key.chunkZPos << 4); + view.setOutputOffset(output.getBufferOffset() / 4); + + return true; + }); + + GPUBuffer uniformGPU = alloc.uniform(uniforms); + + alloc.bindSSBO(0); + uniformGPU.bind(1); + constants.bindSSBO(2); + + glDispatchCompute(submissions.length, 1, 1); + + alloc.unbindSSBO(0); + uniformGPU.unbind(1); + constants.unbindSSBO(2); + + glUseProgram(0); + + return results; + } } } 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 index 5c1605d5..b3c5080f 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/climate/NoiseClimateAxis.java +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/climate/NoiseClimateAxis.java @@ -2,9 +2,11 @@ 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.ScaledNoise; +import com.cardinalstar.cubicchunks.world.worldgen.noise.ScaledSampler; /** * ClimateAxis implementation backed by a ScaledNoise(OctavesSampler). @@ -16,27 +18,18 @@ */ public class NoiseClimateAxis implements ClimateAxis { - private static final int LUT_SIZE = 1024; - private final String name; private final NoiseSampler noise; - private final float[] remapLut; - /** Sum of octave amplitudes; used to normalise FBM output to [-1, 1] before LUT lookup. */ - private final double amplitudeNorm; /** * @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) - * @param remapExponent Power-curve exponent for distribution remapping; <1 spreads extremes */ - public NoiseClimateAxis(String name, long seed, int octaves, double frequency, double remapExponent) { + public NoiseClimateAxis(String name, long seed, int octaves, double frequency) { this.name = name; - this.noise = new ScaledNoise(new OctavesSampler(new Random(seed), octaves), frequency); - this.remapLut = buildLut(remapExponent); - // Sum of amplitudes for n octaves: 2 * (1 - 0.5^n) - this.amplitudeNorm = 2.0 * (1.0 - Math.pow(0.5, octaves)); + this.noise = new NormalizedSampler(new ScaledSampler(new OctavesSampler(new Random(seed), octaves), frequency)); } @Override @@ -46,26 +39,27 @@ public String getName() { @Override public double sample(double x, double z) { - double raw = noise.sample(x, z); - double normalized = raw / amplitudeNorm; - return remap(normalized); + return noise.sample(x, z); } - private float remap(double v) { - v = Math.max(-1.0, Math.min(1.0, v)); - 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 remapLut[lo] + frac * (remapLut[hi] - remapLut[lo]); - } + @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); - 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; + 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 index d35cf154..a2b18598 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/terrain/CCBiomeCache.java +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/terrain/CCBiomeCache.java @@ -1,24 +1,20 @@ package com.cardinalstar.cubicchunks.worldgen.ccenhanced.terrain; -import java.util.LinkedHashMap; -import java.util.Map; - +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.ClimatePoint; 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 5×5 grid is sampled at block offsets {0, 4, 8, 12, 16} in both X and Z - * (indices 0–4 per axis). Position 16 falls in the adjacent chunk — this is valid - * because ClimateSystem depends only on world coordinates, not on loaded chunk data. + * The 16×16 grid provides one biome lookup per block column within the chunk. * *

- * During cube generation, the per-column biome blend weights are bilinearly - * interpolated from the grid using {@code (bx/4, bz/4)} as the grid indices. + * 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 @@ -26,26 +22,15 @@ */ public class CCBiomeCache { - /** Grid size: samples every 4 blocks → 5 sample points covers [0, 16] inclusive. */ - public static final int GRID_SIZE = 5; - public static final int GRID_STRIDE = 4; // blocks between grid samples - 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 LinkedHashMap cache; + private final Long2ObjectLinkedOpenHashMap cache = new Long2ObjectLinkedOpenHashMap<>(CACHE_CAPACITY, 0.75f); public CCBiomeCache(ClimateSystem climate) { this.climate = climate; - this.cache = new LinkedHashMap(CACHE_CAPACITY, 0.75f, true) { - - @Override - protected boolean removeEldestEntry(Map.Entry eldest) { - return size() > CACHE_CAPACITY; - } - }; } /** @@ -53,31 +38,35 @@ protected boolean removeEldestEntry(Map.Entry eldest) * Array is indexed as {@code grid[gx + gz * GRID_SIZE]} where gx, gz ∈ [0, 4]. */ public BiomeLookupResult[] getGrid(int chunkX, int chunkZ) { - long key = packKey(chunkX, chunkZ); - BiomeLookupResult[] grid = cache.get(key); + long key = Coords.packChunk(chunkX, chunkZ); + BiomeLookupResult[] grid = cache.getAndMoveToFirst(key); + if (grid == null) { grid = computeGrid(chunkX, chunkZ); - cache.put(key, grid); + cache.putAndMoveToFirst(key, grid); + + while (cache.size() > CACHE_CAPACITY) cache.removeLast(); } + return grid; } private BiomeLookupResult[] computeGrid(int chunkX, int chunkZ) { - BiomeLookupResult[] grid = new BiomeLookupResult[GRID_SIZE * GRID_SIZE]; - int originX = chunkX * 16; - int originZ = chunkZ * 16; - for (int gi = 0; gi < GRID_SIZE; gi++) { - for (int gj = 0; gj < GRID_SIZE; gj++) { - double bx = originX + gi * GRID_STRIDE; - double bz = originZ + gj * GRID_STRIDE; - ClimatePoint cp = climate.sample(bx, bz); - grid[gi + gj * GRID_SIZE] = CCBiomeRegistry.lookup(cp.values, 3); + 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; - } - private static long packKey(int chunkX, int chunkZ) { - return ((long) chunkX << 32) | (chunkZ & 0xFFFFFFFFL); + 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 index 3dbdef1f..f521c2f6 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/terrain/CCTerrainGenerator.java +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/terrain/CCTerrainGenerator.java @@ -12,7 +12,7 @@ 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; import com.cardinalstar.cubicchunks.worldgen.ccenhanced.biome.BiomeLookupResult; import com.gtnewhorizon.gtnhlib.hash.Fnv1a64; @@ -24,25 +24,18 @@ * (grass/dirt/sand) and 3D perturbation are added in subsequent steps. * *

- * Heightmap formula (SDD §4.3): - * + * Heightmap formula: + * *

- * surfaceY = SEA_LEVEL + blendedRoot * BASE_SCALE + blendedVar * hvNoise * VAR_SCALE
+ * 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 { - // Heightmap formula constants - public static final int SEA_LEVEL = 64; - public static final float BASE_SCALE = 64.0f; - public static final float VAR_SCALE = 64.0f; - - /** Sum of amplitudes for 4 octaves: 2*(1 - 0.5^4) = 1.875. Used to normalize hvNoise. */ - private static final double HV_AMP_NORM = 2.0 * (1.0 - Math.pow(0.5, 4)); - /** HV noise seed slot — distinct from the climate axis slots (0–3) and warp slot (-1). */ private static final long HV_SEED_SLOT = 100L; @@ -51,7 +44,7 @@ public class CCTerrainGenerator { 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 ScaledNoise(new OctavesSampler(new Random(hvSeed), 4), 1.0 / 400.0); + hvNoise = new ScaledSampler(new OctavesSampler(new Random(hvSeed), 4), 1.0 / 400.0); } // ------------------------------------------------------------------------- @@ -66,13 +59,12 @@ public ColumnContext computeColumnContext(Chunk chunk, int chunkX, int chunkZ, C BiomeLookupResult[] grid = biomeCache.getGrid(chunkX, chunkZ); - // Pre-compute blended terrain params at each of the 25 grid points. - // This avoids calling blend() 256 times; only 25 blend computations needed. - float[] gridRoot = new float[CCBiomeCache.GRID_SIZE * CCBiomeCache.GRID_SIZE]; - float[] gridVar = new float[CCBiomeCache.GRID_SIZE * CCBiomeCache.GRID_SIZE]; - for (int gi = 0; gi < CCBiomeCache.GRID_SIZE; gi++) { - for (int gj = 0; gj < CCBiomeCache.GRID_SIZE; gj++) { - int idx = gi + gj * CCBiomeCache.GRID_SIZE; + 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); @@ -82,23 +74,17 @@ public ColumnContext computeColumnContext(Chunk chunk, int chunkX, int chunkZ, C int originX = chunkX * 16; int originZ = chunkZ * 16; - int[] surfaceY = new int[256]; + 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 gx = bx / CCBiomeCache.GRID_STRIDE; - int gz = bz / CCBiomeCache.GRID_STRIDE; - float fx = (bx % CCBiomeCache.GRID_STRIDE) / (float) CCBiomeCache.GRID_STRIDE; - float fz = (bz % CCBiomeCache.GRID_STRIDE) / (float) CCBiomeCache.GRID_STRIDE; + int idx = bz << 4 | bx; - float root = bilerp(gridRoot, gx, gz, fx, fz); - float var = bilerp(gridVar, gx, gz, fx, fz); + double hv = hvNoise.sample(originX + bx, originZ + bz); - double hv = hvNoise.sample(originX + bx, originZ + bz) / HV_AMP_NORM; - - int sy = (int) (SEA_LEVEL + root * BASE_SCALE + var * (float) hv * VAR_SCALE); + int sy = (int) (gridRoot[idx] + gridVar[idx] * (float) hv); surfaceY[bx + bz * 16] = sy; // Write to vanilla heightmap. Convention: store surfaceY (first-air-Y). @@ -161,18 +147,4 @@ public ExtendedBlockStorage buildEbs(World world, ColumnContext ctx, int cubeY) return ebs; } - - // ------------------------------------------------------------------------- - // Helpers - // ------------------------------------------------------------------------- - - /** Bilinear interpolation over a flat GRID_SIZE×GRID_SIZE float array. */ - private static float bilerp(float[] grid, int gx, int gz, float fx, float fz) { - int g = CCBiomeCache.GRID_SIZE; - float v00 = grid[gx + gz * g]; - float v10 = grid[(gx + 1) + gz * g]; - float v01 = grid[gx + (gz + 1) * g]; - float v11 = grid[(gx + 1) + (gz + 1) * g]; - return v00 * (1 - fx) * (1 - fz) + v10 * fx * (1 - fz) + v01 * (1 - fx) * fz + v11 * fx * fz; - } } From ad6da3523fa5bbdf4ad7d4f67183f20a8489bf79 Mon Sep 17 00:00:00 2001 From: RecursivePineapple Date: Thu, 9 Jul 2026 14:45:39 -0400 Subject: [PATCH 8/8] stuff --- .../cubicchunks/codegen/FieldType.java | 1 + .../cubicchunks/codegen/GenerateStructs.java | 57 +- .../cubicchunks/codegen/Struct.java | 151 +++- .../cubicchunks/codegen/StructField.java | 3 +- .../cardinalstar/cubicchunks/CubicChunks.java | 4 +- .../cubicchunks/CubicChunksConfig.java | 6 +- .../api/worldgen/hwaccel/ComputeContext.java | 5 - .../api/worldgen/hwaccel/ComputePipeline.java | 129 +++ .../api/worldgen/hwaccel/ComputePlan.java | 27 +- .../api/worldgen/hwaccel/KernelBuilder.java | 134 ++- .../hwaccel/KernelConstantBuilder.java | 63 -- .../api/worldgen/hwaccel/KernelContext.java | 516 +++++++++-- .../api/worldgen/hwaccel/KernelExecutor.java | 83 +- .../api/worldgen/hwaccel/KernelJob.java | 5 +- .../api/worldgen/hwaccel/KernelScheduler.java | 806 ++++++++++++++---- .../worldgen/hwaccel/KernelSubmission.java | 2 +- .../hwaccel/Noise2DKernelExecutor.java | 117 +-- .../api/worldgen/hwaccel/PrimitiveBuffer.java | 3 +- .../worldgen/hwaccel/PushConstantLayout.java | 287 +++++++ .../api/worldgen/hwaccel/RenderdocAPI.java | 18 + .../api/worldgen/hwaccel/ShaderCache.java | 157 ++++ .../api/worldgen/hwaccel/SpirVCompiler.java | 75 ++ .../hwaccel/StandardKernelExecutor.java | 115 +++ .../api/worldgen/hwaccel/VulkanBuffer.java | 229 +++++ .../worldgen/hwaccel/VulkanPipelineCache.java | 94 ++ .../api/worldgen/hwaccel/WorkDomain.java | 27 - .../hwaccel/buffer/ArenaHardwareBuffer.java | 119 --- .../hwaccel/buffer/BufferAccessor.java | 2 + .../hwaccel/buffer/BufferAllocator.java | 29 +- .../hwaccel/buffer/BufferDataType.java | 21 + .../hwaccel/buffer/BufferDescriptor.java | 26 +- .../worldgen/hwaccel/buffer/BufferLayout.java | 15 + .../hwaccel/buffer/ConstantBuffer.java | 67 ++ .../buffer/ConstantHardwareBuffer.java | 114 --- .../worldgen/hwaccel/buffer/GPUBuffer.java | 92 +- .../hwaccel/buffer/HardwareBuffer.java | 81 -- .../hwaccel/buffer/HardwareSubBuffer.java | 76 -- .../hwaccel/buffer/OffsetBufferAccessor.java | 23 + .../buffer/TransformingBufferAccessor.java | 24 + .../hwaccel/buffer/VulkanArenaAllocator.java | 36 + .../hwaccel/buffer/VulkanArenaSlot.java | 52 ++ .../hwaccel/buffer/VulkanConstantPool.java | 122 +++ .../lighting/FirstLightProcessor.java | 27 +- .../cubicchunks/lighting/LightingManager.java | 6 +- .../lighting/phosphor/LightingHooks.java | 9 +- .../early/common/MixinBlock_Lighting.java | 6 +- .../mixin/early/common/MixinEBSID_EID.java | 2 - .../early/common/MixinEBSID_Vanilla.java | 2 +- .../mixin/early/common/MixinWorld.java | 6 +- .../cubicchunks/mixin/ext/EBSIDAccessor.java | 1 + .../network/PacketEncoderCube.java | 12 +- .../PacketEncoderUpdateVisualizedBoxes.java | 3 +- .../server/CubeProviderServer.java | 5 +- .../server/WorldSyncStateMachine.java | 3 +- .../server/chunkio/CCNBTUtils.java | 5 +- .../server/chunkio/CubeLoaderServer.java | 49 +- .../util/CubeStatusVisualizer.java | 17 +- .../cubicchunks/util/DataUtils.java | 8 + .../cubicchunks/util/JavaUtils.java | 4 +- .../util/boxvisualizer/BoxVisualizer.java | 12 +- .../util/boxvisualizer/QuadSorter.java | 27 +- .../util/boxvisualizer/VisualizedBox.java | 8 +- .../boxvisualizer/VisualizedBoxRenderer.java | 20 +- .../cubicchunks/world/layer/LayeredWorld.java | 71 +- .../cubicchunks/world/layer/WorldLayer.java | 3 + .../worldgen/noise/NormalizedSampler.java | 36 +- .../world/worldgen/noise/OctavesSampler.java | 42 +- .../world/worldgen/noise/ScaledSampler.java | 9 +- .../world/worldgen/noise/SimplexSampler.java | 173 ++-- .../ccenhanced/CCEnhancedWorldGenerator.java | 251 +++--- .../biome/BiomeDistanceKernelExecutor.java | 139 +-- .../biome/BiomeLookupKernelExecutor.java | 132 +-- .../ccenhanced/biome/CCBiomeRegistry.java | 6 +- .../biome/HeightMapKernelExecutor.java | 151 +--- .../ccenhanced/biome/defaults/CCBiomes.java | 4 +- .../ccenhanced/climate/ClimateSystem.java | 148 +--- .../ccenhanced/climate/NoiseClimateAxis.java | 19 +- .../ccenhanced/terrain/CCBiomeCache.java | 5 +- 78 files changed, 3536 insertions(+), 1898 deletions(-) delete mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/ComputeContext.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/ComputePipeline.java delete mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelConstantBuilder.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/PushConstantLayout.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/RenderdocAPI.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/ShaderCache.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/SpirVCompiler.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/StandardKernelExecutor.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/VulkanBuffer.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/VulkanPipelineCache.java delete mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/WorkDomain.java delete mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/ArenaHardwareBuffer.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/BufferLayout.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/ConstantBuffer.java delete mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/ConstantHardwareBuffer.java delete mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/HardwareBuffer.java delete mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/HardwareSubBuffer.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/OffsetBufferAccessor.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/TransformingBufferAccessor.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/VulkanArenaAllocator.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/VulkanArenaSlot.java create mode 100644 src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/VulkanConstantPool.java diff --git a/src/buildSrc/java/com/cardinalstar/cubicchunks/codegen/FieldType.java b/src/buildSrc/java/com/cardinalstar/cubicchunks/codegen/FieldType.java index 07313d4b..10dcd088 100644 --- a/src/buildSrc/java/com/cardinalstar/cubicchunks/codegen/FieldType.java +++ b/src/buildSrc/java/com/cardinalstar/cubicchunks/codegen/FieldType.java @@ -1,6 +1,7 @@ package com.cardinalstar.cubicchunks.codegen; public enum FieldType { + i32, u32, i64, diff --git a/src/buildSrc/java/com/cardinalstar/cubicchunks/codegen/GenerateStructs.java b/src/buildSrc/java/com/cardinalstar/cubicchunks/codegen/GenerateStructs.java index 1ec8e27b..e319ab2b 100644 --- a/src/buildSrc/java/com/cardinalstar/cubicchunks/codegen/GenerateStructs.java +++ b/src/buildSrc/java/com/cardinalstar/cubicchunks/codegen/GenerateStructs.java @@ -12,30 +12,35 @@ public static void main(String[] args) throws IOException { 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"); + 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 index 66e9d9de..e9b82ae1 100644 --- a/src/buildSrc/java/com/cardinalstar/cubicchunks/codegen/Struct.java +++ b/src/buildSrc/java/com/cardinalstar/cubicchunks/codegen/Struct.java @@ -44,7 +44,9 @@ public void writePrimitiveBuffer() throws IOException { String viewName = this.name + "PrimitiveView"; String bufferName = this.name + "PrimitiveBuffer"; - code.append("package ").append(pkg).append(";\n"); + 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"); @@ -63,15 +65,21 @@ public void writePrimitiveBuffer() throws IOException { .append(viewName) .append("> {\n"); code.append("\n"); - code.append(" public static final int INT_STRIDE = ").append(this.stride).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(" private final ObjectPooler<") + .append(viewName) + .append("> pool;\n"); code.append("\n"); - code.append(" public ").append(bufferName).append("(int size) {\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"); @@ -79,8 +87,12 @@ public void writePrimitiveBuffer() throws IOException { 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(" 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"); @@ -99,14 +111,20 @@ public void writePrimitiveBuffer() throws IOException { 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(" 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(" public void forEachSlow(TIntObjectProcedure<") + .append(viewName) + .append("> fn) {\n"); code.append(" for (int i = 0; i < this.size; i++) {\n"); code.append(" ") .append(viewName) @@ -119,8 +137,14 @@ public void writePrimitiveBuffer() throws IOException { 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(" 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"); @@ -128,8 +152,12 @@ public void writePrimitiveBuffer() throws IOException { 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(" 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"); @@ -138,7 +166,9 @@ public void writePrimitiveBuffer() throws IOException { code.append(" }\n"); code.append("\n"); code.append(" @Override\n"); - code.append(" public ").append(viewName).append(" next() {\n"); + code.append(" public ") + .append(viewName) + .append(" next() {\n"); code.append(" if (!hasNext()) throw new NoSuchElementException();\n"); code.append(" ") .append(viewName) @@ -152,8 +182,12 @@ public void writePrimitiveBuffer() throws IOException { 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(" 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) @@ -167,7 +201,9 @@ public void writePrimitiveBuffer() throws IOException { code.append(" }\n"); code.append("\n"); code.append(" @Override\n"); - code.append(" public ").append(viewName).append(" next() {\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"); @@ -178,7 +214,8 @@ public void writePrimitiveBuffer() throws IOException { 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( + " 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"); @@ -186,14 +223,16 @@ public void writePrimitiveBuffer() throws IOException { 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( + " 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(); + 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)); @@ -202,7 +241,11 @@ public void writePrimitiveBuffer() throws IOException { Path file = Paths.get(name + "PrimitiveBuffer.java"); - Files.write(src.resolve(next).resolve(file), code.toString().getBytes(StandardCharsets.UTF_8)); + Files.write( + src.resolve(next) + .resolve(file), + code.toString() + .getBytes(StandardCharsets.UTF_8)); } public void writePrimitiveView() throws IOException { @@ -210,20 +253,30 @@ public void writePrimitiveView() throws IOException { String name = this.name + "PrimitiveView"; - code.append("package ").append(pkg).append(";\n"); + 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("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 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(" ") + .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"); @@ -342,7 +395,8 @@ public void writePrimitiveView() throws IOException { code.append("}\n"); - Path src = Paths.get("build", "generated", "sources", "structs").toAbsolutePath(); + 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)); @@ -351,7 +405,11 @@ public void writePrimitiveView() throws IOException { Path file = Paths.get(name + ".java"); - Files.write(src.resolve(next).resolve(file), code.toString().getBytes(StandardCharsets.UTF_8)); + Files.write( + src.resolve(next) + .resolve(file), + code.toString() + .getBytes(StandardCharsets.UTF_8)); } public void writeGLStruct() throws IOException { @@ -360,16 +418,24 @@ public void writeGLStruct() throws IOException { // 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"); + 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(" uint _pad") + .append(padCount++) + .append(";\n"); } - glsl.append(" ").append(field.type.glslType()).append(" ").append(field.name).append(";\n"); + glsl.append(" ") + .append(field.type.glslType()) + .append(" ") + .append(field.name) + .append(";\n"); cursor = field.glByteOffset + field.type.byteSize(); } @@ -378,25 +444,34 @@ public void writeGLStruct() throws IOException { // 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("package ") + .append(pkg) + .append(";\n"); code.append("\n"); - code.append("public final class ").append(glStructName).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"); + 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(); + 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) - ); + 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 index 9c140c3e..95315e60 100644 --- a/src/buildSrc/java/com/cardinalstar/cubicchunks/codegen/StructField.java +++ b/src/buildSrc/java/com/cardinalstar/cubicchunks/codegen/StructField.java @@ -19,6 +19,7 @@ public class StructField { } public String getPascalCase() { - return name.substring(0, 1).toUpperCase(Locale.ROOT) + name.substring(1); + 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 abd0d7f7..87ad7382 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/CubicChunks.java +++ b/src/main/java/com/cardinalstar/cubicchunks/CubicChunks.java @@ -57,6 +57,7 @@ import com.falsepattern.chunk.api.DataRegistry; import com.gtnewhorizon.gtnhlib.config.ConfigException; import com.gtnewhorizon.gtnhlib.config.ConfigurationManager; + import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.ICrashCallable; import cpw.mods.fml.common.Loader; @@ -126,7 +127,8 @@ public void preInit(FMLPreInitializationEvent e) { LOGGER.debug("Registered world types"); - if (FMLCommonHandler.instance().getSide() == Side.CLIENT) { + if (FMLCommonHandler.instance() + .getSide() == Side.CLIENT) { KernelContext.initClient(); } else { KernelContext.initServer(); 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/worldgen/hwaccel/ComputeContext.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/ComputeContext.java deleted file mode 100644 index 1c66ddd1..00000000 --- a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/ComputeContext.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; - -public interface ComputeContext { - -} 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 index 4d6cfb8c..fb86fb89 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/ComputePlan.java +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/ComputePlan.java @@ -6,7 +6,9 @@ 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 { @@ -21,22 +23,39 @@ public BufferDescriptor describeBuffer(KernelSubmissionToken submission, BufferD return describeBuffer(submission, dataType, lenX, 1, 1); } - public BufferDescriptor describeBuffer(KernelSubmissionToken submission, BufferDataType dataType, int lenX, int lenY) { + 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) { + 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, Key key, Map inputs) { + 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; + 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); 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 index c7774158..474bc126 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelBuilder.java +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelBuilder.java @@ -1,17 +1,147 @@ package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.ConstantHardwareBuffer; +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 ConstantHardwareBuffer constants = new ConstantHardwareBuffer("constants"); + 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/KernelConstantBuilder.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelConstantBuilder.java deleted file mode 100644 index df1acce9..00000000 --- a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelConstantBuilder.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; - -import java.util.ArrayList; -import java.util.List; - -public class KernelConstantBuilder { - - private final String type, name; - private final List lines = new ArrayList<>(); - - public KernelConstantBuilder(String type, String name) { - this.type = type; - this.name = name; - } - - public void annotate(String comment) { - lines.add(comment); - } - - public void add(float... values) { - lines.add(values); - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - - int len = 0; - - for (var line : lines) { - if (line instanceof float[] values) { - len += values.length; - } - } - - builder.append("const ").append(type).append(" ").append(name).append("[").append(len).append("] = ").append(type).append("[").append(len).append("]("); - - boolean needsComma = false; - - for (var line : lines) { - if (line instanceof String comment) { - builder.append(" // ").append(comment).append("\n"); - } - - if (line instanceof float[] values) { - builder.append(" "); - - for (var value : values) { - if (needsComma) builder.append(","); - - builder.append(value).append("f"); - needsComma = true; - } - - builder.append("\n"); - } - } - - builder.append(");"); - - return builder.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 index 9a63ba36..9edd7bfb 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelContext.java +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelContext.java @@ -1,25 +1,97 @@ package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; -import static org.lwjgl.opengl.GL11.glGetInteger; -import static org.lwjgl.opengl.GL43.GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT; - -import org.lwjgl.opengl.GL11; -import org.lwjgl.sdl.SDLHints; -import org.lwjgl.sdl.SDLInit; -import org.lwjgl.sdl.SDLVideo; +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 com.cardinalstar.cubicchunks.util.JavaUtils; -import cpw.mods.fml.common.FMLCommonHandler; -import cpw.mods.fml.common.eventhandler.SubscribeEvent; -import cpw.mods.fml.common.gameevent.TickEvent.Phase; -import cpw.mods.fml.common.gameevent.TickEvent.RenderTickEvent; + 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; @@ -28,113 +100,411 @@ public class KernelContext { @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 int SSBOAlignment; + private static long vmaAllocator; @SideOnly(Side.CLIENT) public static void initClient() { - if (!isContextValid()) { - CubicChunks.LOGGER.info("Render context is invalid, disabling compute shaders"); + startWorkerThread(); + } - return; - } + @SideOnly(Side.SERVER) + public static void initServer() { + startWorkerThread(); + } - CubicChunks.LOGGER.info("Render context is valid, using it as the compute context"); + private static void startWorkerThread() { + worker = new Thread(KernelContext::init); + worker.setName("CC-WG-Dispatcher"); + worker.setDaemon(true); + worker.start(); + } - SSBOAlignment = glGetInteger(GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT); + 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); - enabled = true; + CubicChunks.LOGGER.info("Successfully created offscreen compute context"); + enabled = true; scheduler = new KernelScheduler(); - FMLCommonHandler.instance().bus().register(new ClientEventHandler()); + 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); + } } - public static class ClientEventHandler { + 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; + } - @SubscribeEvent - public void tick(RenderTickEvent event) { - if (event.phase != Phase.START) return; + 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"; + } - scheduler.tick(); - } + 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; } - @SideOnly(Side.SERVER) - public static void initServer() { - worker = new Thread(() -> { - SDLHints.SDL_SetHint("SDL_VIDEODRIVER", "offscreen"); - SDLInit.SDL_Init(SDLInit.SDL_INIT_VIDEO); + private static PointerBuffer checkLayers(MemoryStack stack, Set availableLayers, String... requiredLayers) { + for (String req : requiredLayers) { + if (!availableLayers.contains(req)) return null; + } - long window = SDLVideo.SDL_CreateWindow("", 1, 1, SDLVideo.SDL_WINDOW_OPENGL | SDLVideo.SDL_WINDOW_HIDDEN); - long context = SDLVideo.SDL_GL_CreateContext(window); - SDLVideo.SDL_GL_MakeCurrent(window, context); + PointerBuffer buffer = stack.mallocPointer(requiredLayers.length); -// GL.create(SDLVideo::SDL_GL_GetProcAddress); -// GL.createCapabilities(); + for (int i = 0; i < requiredLayers.length; i++) { + buffer.put(i, stack.ASCII(requiredLayers[i])); + } - if (!isContextValid()) { - CubicChunks.LOGGER.info("Server offscreen context does not support compute shaders: disabling"); - return; + 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."); + } } - CubicChunks.LOGGER.info("Successfully created server offscreen context that supports OpenGL compute shaders"); + check(vkEnumerateInstanceExtensionProperties((String) null, IP, null)); - enabled = true; + List requiredInstanceExtensions = new ArrayList<>(); + requiredInstanceExtensions.add(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME); - scheduler = new KernelScheduler(); + if (IP.get(0) > 0) { + VkExtensionProperties.Buffer instance_extensions = VkExtensionProperties.malloc(IP.get(0), stack); + check(vkEnumerateInstanceExtensionProperties((String) null, IP, instance_extensions)); - try { - while (true) { - scheduler.tick(); + Set availableExtensions = instance_extensions.stream() + .map(VkExtensionProperties::extensionNameString) + .collect(Collectors.toSet()); - Thread.yield(); - JavaUtils.onSpinWait(); // TODO: blocking + if (availableExtensions.contains(VK_EXT_DEBUG_UTILS_EXTENSION_NAME) && ENABLE_VALIDATION) { + requiredInstanceExtensions.add(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); } - } catch (Throwable t) { - CubicChunks.LOGGER.error("CC-WG-Dispatcher failed with the following error", t); } - SDLVideo.SDL_GL_DestroyContext(context); - SDLVideo.SDL_DestroyWindow(window); - }); + PointerBuffer ppEnabledExtensionNames = stack.mallocPointer(requiredInstanceExtensions.size()); - worker.setName("CC-WG-Dispatcher"); - worker.setDaemon(true); + for (String ext : requiredInstanceExtensions) { + ppEnabledExtensionNames.put(stack.UTF8(ext)); + } - worker.start(); + 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 boolean isContextValid() { - String versionRaw = GL11.glGetString(GL11.GL_VERSION); + 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)); - CubicChunks.LOGGER.info("GL Version: {}", versionRaw); + if (IP.get(0) > 0) { + PointerBuffer physical_devices = stack.mallocPointer(IP.get(0)); + check(vkEnumeratePhysicalDevices(instance, IP, physical_devices)); - String[] version = versionRaw.split(" +"); + VkPhysicalDeviceFeatures features = VkPhysicalDeviceFeatures.calloc(stack); + VkPhysicalDeviceProperties props = VkPhysicalDeviceProperties.calloc(stack); + VkPhysicalDeviceMemoryProperties memProps = VkPhysicalDeviceMemoryProperties.calloc(stack); - String[] parts = version[0].split("\\."); + VkPhysicalDevice best = null; + long bestScore = -1; - if (Integer.parseInt(parts[0]) >= 4 && Integer.parseInt(parts[1]) >= 3) return true; + for (int i = 0; i < IP.get(0); i++) { + VkPhysicalDevice device = new VkPhysicalDevice(physical_devices.get(i), instance); - String extensionsRaw = GL11.glGetString(GL11.GL_EXTENSIONS); + vkGetPhysicalDeviceFeatures(device, features); + vkGetPhysicalDeviceProperties(device, props); + vkGetPhysicalDeviceMemoryProperties(device, memProps); - CubicChunks.LOGGER.info("GL Extensions: {}", extensionsRaw); + long score = scoreDevice(props, memProps); + CubicChunks.LOGGER.info( + "Vulkan device [{}]: type={}, score={}", + props.deviceNameString(), + props.deviceType(), + score); - String[] extensions = extensionsRaw.split(" +"); + if (score > bestScore) { + bestScore = score; + best = device; + } + } - boolean hasComputeShaders = false; - boolean hasSSBOs = false; + if (best == null) { + throw new IllegalStateException("No suitable Vulkan physical device found."); + } - for (var ext : extensions) { - if (ext.equals("GL_ARB_compute_shader")) { - hasComputeShaders = true; + gpu = best; + } else { + throw new IllegalStateException("vkEnumeratePhysicalDevices reported zero accessible devices."); } + } + } - if (ext.equals("GL_ARB_shader_storage_buffer_object")) { - hasSSBOs = true; + 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 hasComputeShaders && hasSSBOs; + 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 index e7f1c8ae..3640ad4d 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelExecutor.java +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelExecutor.java @@ -1,77 +1,36 @@ package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; -import static org.lwjgl.opengl.GL20.GL_COMPILE_STATUS; -import static org.lwjgl.opengl.GL20.GL_LINK_STATUS; -import static org.lwjgl.opengl.GL20.glAttachShader; -import static org.lwjgl.opengl.GL20.glCompileShader; -import static org.lwjgl.opengl.GL20.glCreateProgram; -import static org.lwjgl.opengl.GL20.glCreateShader; -import static org.lwjgl.opengl.GL20.glGetProgramInfoLog; -import static org.lwjgl.opengl.GL20.glGetProgrami; -import static org.lwjgl.opengl.GL20.glGetShaderInfoLog; -import static org.lwjgl.opengl.GL20.glGetShaderi; -import static org.lwjgl.opengl.GL20.glLinkProgram; -import static org.lwjgl.opengl.GL20.glShaderSource; -import static org.lwjgl.opengl.GL43.GL_COMPUTE_SHADER; - +import java.io.Closeable; import java.util.Map; -import net.minecraft.util.MathHelper; +import org.lwjgl.vulkan.VkCommandBuffer; -import com.cardinalstar.cubicchunks.CubicChunks; 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; -public interface KernelExecutor { - - Map getOutputs(ComputePlan plan, KernelSubmissionToken submission, Key key, Map inputs); - - KernelSubmissionResult[] submit(BufferAllocator alloc, KernelSubmission[] submissions); - - static int createProgram(String code) { - int shader = glCreateShader(GL_COMPUTE_SHADER); - glShaderSource(shader, code); - glCompileShader(shader); - - if (glGetShaderi(shader, GL_COMPILE_STATUS) == 0) { - String log = glGetShaderInfoLog(shader, 32000); - - String[] lines = code.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: {}", log, new Exception()); - throw new RuntimeException("Could not compile shader"); - } - - int program = glCreateProgram(); - - glAttachShader(program, shader); - glLinkProgram(program); - - if (glGetProgrami(program, GL_LINK_STATUS) == 0) { - String log = glGetProgramInfoLog(program, 32000); +import me.eigenraven.lwjgl3ify.api.Lwjgl3Aware; - String[] lines = code.split("\n\r?"); +@Lwjgl3Aware +public interface KernelExecutor extends Closeable { - int zeroes = MathHelper.ceiling_double_int(Math.log10(lines.length)); + @CallingThread(ThreadType.WORKER) + @Override + void close(); - for (int i = 0; i < lines.length; i++) { - lines[i] = String.format("%0" + zeroes + "d:", i + 1) + lines[i]; - } + @CallingThread(ThreadType.WORKER) + boolean isCompiled(); - CubicChunks.LOGGER.error("Shader code:\n{}", String.join("\n", lines)); + @CallingThread(ThreadType.WORKER) + void compile(ConstantBuffer constants); - CubicChunks.LOGGER.error("Could not link shader: {}", log, new Exception()); - throw new RuntimeException("Could not link shader"); - } + @CallingThread(ThreadType.SERVER) + Map getOutputs(ComputePlan plan, KernelSubmissionToken submission, Key key, + Map inputs); - return program; - } + @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 index 5e75749a..e647748c 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelJob.java +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelJob.java @@ -6,8 +6,7 @@ import com.github.bsideup.jabel.Desugar; @Desugar -record KernelJob( - KernelSubmissionToken submission, Map inputs, Map outputs -) { +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 index b1b26fec..843bff8d 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelScheduler.java +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelScheduler.java @@ -1,68 +1,131 @@ package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; -import static org.lwjgl.opengl.ARBSync.glClientWaitSync; -import static org.lwjgl.opengl.ARBSync.glDeleteSync; -import static org.lwjgl.opengl.ARBSync.glFenceSync; -import static org.lwjgl.opengl.GL11.glFlush; -import static org.lwjgl.opengl.GL11.glGetError; -import static org.lwjgl.opengl.GL30.glBindBufferBase; -import static org.lwjgl.opengl.GL32.GL_ALREADY_SIGNALED; -import static org.lwjgl.opengl.GL32.GL_SYNC_GPU_COMMANDS_COMPLETE; -import static org.lwjgl.opengl.GL42.glMemoryBarrier; -import static org.lwjgl.opengl.GL43.GL_SHADER_STORAGE_BARRIER_BIT; -import static org.lwjgl.opengl.GL43.GL_SHADER_STORAGE_BUFFER; -import static org.lwjgl.opengl.GL44.GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT; - +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 java.util.concurrent.locks.Condition; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.lwjgl.opengl.GLSync; -import org.lwjgl.util.glu.GLU; +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.ArenaHardwareBuffer; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferAllocator; -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.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 { +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 {} + private interface Task { + } @Desugar private record SubmitTask(List plans, AtomicBoolean ready) implements Task {} + @Desugar - private record ExecTask(Runnable fn, Lock lock, Condition condition) implements Task {} + private record ExecTask(Runnable fn, AtomicBoolean done) implements Task {} public static final Logger LOGGER = LogManager.getLogger("CC-KernelScheduler"); @@ -80,19 +143,92 @@ private record ExecTask(Runnable fn, Lock lock, Condition condition) implements private long gpuBudgetNs = 20_000_000L; // 20 ms default, configurable - private final Runnable pollForTasks = this::poll; - private Runnable state = pollForTasks; + 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<>(); - private final ArenaHardwareBuffer arena = new ArenaHardwareBuffer(); + 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; } + public void setGpuBudget(long ns) { + this.gpuBudgetNs = ns; + } + @CallingThread(ThreadType.SERVER) - public long getGpuBudget() { return gpuBudgetNs; } + public long getGpuBudget() { + return gpuBudgetNs; + } /** * Estimates the GPU wall-clock cost of executing all kernels in {@code plan} based on @@ -104,7 +240,10 @@ public long estimatePlanCost(ComputePlan plan) { synchronized (emaLock) { long total = 0; for (var e : plan.submits.int2ObjectEntrySet()) { - total += getEstimateNs(e.getValue().submission().executor()); + total += getEstimateNs( + e.getValue() + .submission() + .executor()); } return total; } @@ -130,16 +269,11 @@ public void submit(List plans) { @CallingThread(ThreadType.SERVER) public void runAndWait(Runnable fn) { - Lock lock = new ReentrantLock(); - Condition condition = lock.newCondition(); - tasks.add(new ExecTask(fn, lock, condition)); - try { - lock.lock(); - condition.await(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } finally { - lock.unlock(); + AtomicBoolean done = new AtomicBoolean(false); + tasks.add(new ExecTask(fn, done)); + while (!done.get()) { + Thread.yield(); + JavaUtils.onSpinWait(); } } @@ -148,30 +282,34 @@ public void processResults() { TerminalResult result; while ((result = results.poll()) != null) { - result.task().execute(result.data()); + result.task() + .execute(result.data()); } } - @CallingThread(ThreadType.CLIENT) - public void tick() { - state.run(); + @CallingThread(ThreadType.SERVER) + public void compileExecutor(KernelExecutor executor) { + runAndWait(() -> { executor.compile(this.constants); }); } @CallingThread(ThreadType.CLIENT) - private void poll() { - Task task; + public void run() { + while (true) { + Task task; + + try { + task = tasks.take(); + } catch (InterruptedException e) { + continue; + } - while ((task = this.tasks.poll()) != null) { if (task instanceof SubmitTask submit) { this.submitPlans(submit.plans, submit.ready); } else if (task instanceof ExecTask exec) { - exec.fn().run(); - try { - exec.lock().lock(); - exec.condition().signalAll(); - } finally { - exec.lock().unlock(); - } + exec.fn() + .run(); + exec.done() + .set(true); } } } @@ -180,19 +318,15 @@ private void poll() { private void submitPlans(List plans, AtomicBoolean ready) { LOGGER.info("Processing batch with {} plans", plans.size()); - // --- Step A: build the global job table --- - // - // Each plan's submission IDs start at 0. We assign a planBase (cumulative submission - // count) and bufferBase (cumulative buffer-slot count) so that every job and every - // buffer slot has a globally unique index across the whole batch. - int totalJobs = 0; + int totalJobs = 0; int totalBuffers = 0; - int[] planBase = new int[plans.size()]; + int[] planBase = new int[plans.size()]; int[] bufferBase = new int[plans.size()]; + for (int p = 0; p < plans.size(); p++) { - planBase[p] = totalJobs; + planBase[p] = totalJobs; bufferBase[p] = totalBuffers; - totalJobs += plans.get(p).nextSubmission; + totalJobs += plans.get(p).nextSubmission; totalBuffers += plans.get(p).nextBuffer; } @@ -200,47 +334,105 @@ private void submitPlans(List plans, AtomicBoolean ready) { 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()); + 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 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(), KernelContext.getSSBOAlignment()); + for (var output : job.job.outputs() + .values()) { + arenaLength += MathUtil.alignTo(output.getBufferLength(), 16); } } - checkError(); - - // --- Step C: wave dispatch --- - // - // All jobs at the same level are independent of each other and can be dispatched - // without a barrier between them. A single barrier is issued between consecutive - // levels. This amortises the barrier cost across all plans in the batch: for N chunks - // the pipeline uses 6 barriers total instead of 6 per chunk. - arenaLength *= 2; - if (this.arena.getByteLen() < arenaLength) { - this.arena.resize(arenaLength); - } else { - this.arena.reset(); - } - - Allocator alloc = new Allocator(arena); - 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<>(); @@ -255,11 +447,15 @@ private void submitPlans(List plans, AtomicBoolean ready) { // 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()]); + for (Map.Entry e : job.inputs() + .entrySet()) { + inputs.put( + e.getKey(), + buffers[globalJobs[i].bufferBase() + e.getValue() + .bufferId()]); } - //noinspection unchecked + // noinspection unchecked s.add(Pair.of(globalJobs[i], new KernelSubmission(submission.key(), inputs))); dispatchCounts.addTo(submission.executor(), 1); @@ -267,108 +463,263 @@ private void submitPlans(List plans, AtomicBoolean ready) { } submits.forEach((exec, submissionList) -> { - KernelSubmission[] submissionArray = DataUtils.mapToArray(submissionList, KernelSubmission[]::new, Pair::right); - - checkError("pre " + exec.toString()); + 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())); + } + } - @SuppressWarnings("unchecked") - KernelSubmissionResult[] results = exec.submit(alloc, submissionArray); + 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); + } + }); + } - checkError("post " + exec.toString()); + // --- 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. - for (int i = 0; i < results.length; i++) { - var pair = submissionList.get(i); + // Map from global buffer index to readback byte offset. + Int2IntOpenHashMap terminalReadbackOffsets = new Int2IntOpenHashMap(); + int readbackOffset = 0; - // 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()); - buffers[pair.left().bufferBase() + outDesc.bufferId()] = e.getValue(); + 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(); } } - }); - - glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); + } } - GLSync sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); - glFlush(); + List copies = new ArrayList<>(); - checkError(); + terminalReadbackOffsets.forEach((int bufId, int offset) -> { + GPUBuffer buf = buffers[bufId]; + copies.add(new CopyRegion(buf.getBufferOffset(), offset, buf.getBufferLength())); + }); - this.state = () -> { - if (glClientWaitSync(sync, 0, 0) == GL_ALREADY_SIGNALED) { - checkError(); + submitVulkanBatch(commandBuffer, copies); - this.state = pollForTasks; + // GPU is now idle; read terminal data from the persistently mapped readback buffer. + long batchElapsed = System.nanoTime() - batchStart; + updateEMAs(dispatchCounts, batchElapsed); - glDeleteSync(sync); + LOGGER.info("Batch took {} ms", String.format("%.2f", batchElapsed / 1000000f)); - checkError(); + // 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())); + } + } - long batchElapsed = System.nanoTime() - batchStart; - updateEMAs(dispatchCounts, batchElapsed); + ready.set(true); + } - LOGGER.info("Batch took {} ms", batchElapsed / 1000000); + // ------------------------------------------------------------------------- + // Vulkan dispatch helpers + // ------------------------------------------------------------------------- - // 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]; + @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); + } + } - // Cache downloads within a plan: multiple terminals may share the same buffer. - ByteBuffer[] downloads = new ByteBuffer[plan.nextBuffer]; + @CallingThread(ThreadType.CLIENT) + private void insertOutputBarriers(VkCommandBuffer cmd, List outputs) { + if (outputs.isEmpty()) return; - for (var terminal : plan.terminals) { - Map inputs = resolveDownloads(terminal.inputs(), bb, buffers, downloads); - this.results.add(new TerminalResult(inputs, terminal.task())); - } - } + long arenaBuf = arena.buffer(); - checkError(); + VkBufferMemoryBarrier.Buffer barriers = VkBufferMemoryBarrier.calloc(outputs.size()); - ready.set(true); + 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()); } - }; - } - - private void checkError() { - int error = glGetError(); - if (error != 0) { - LOGGER.error("GL Error: {}", GLU.gluErrorString(error), new Exception()); + vkCmdPipelineBarrier( + cmd, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + 0, + null, + barriers, + null); + } finally { + barriers.free(); } } - private void checkError(String info) { - int error = glGetError(); + @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()); - if (error != 0) { - LOGGER.error("GL Error ({}): {}", info, GLU.gluErrorString(error), new Exception()); + 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)); } } - /// Copies any not-yet-downloaded buffers referenced by `terminalInputs` from the mapped arena - /// into fresh direct ByteBuffers and returns them. + /// 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 resolveDownloads( - Map terminalInputs, - int bufferBaseOffset, - GPUBuffer[] buffers, - ByteBuffer[] downloads - ) { + 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(); + int localBufId = input.getValue() + .bufferId(); if (downloads[localBufId] == null) { - GPUBuffer buf = buffers[bufferBaseOffset + localBufId]; + int globalBufIdx = bufferBaseOffset + localBufId; + GPUBuffer buf = buffers[globalBufIdx]; + int byteLen = buf.getBufferLength(); + int rbOffset = terminalReadbackOffsets.get(globalBufIdx); - ByteBuffer downloaded = ByteBuffer.allocateDirect(buf.getBufferLength()).order(ByteOrder.nativeOrder()); + ByteBuffer slice = ByteBuffer.allocateDirect(byteLen) + .order(ByteOrder.nativeOrder()); - buf.download(downloaded); + MemoryUtil + .memCopy(MemoryUtil.memAddress(readbackMapped) + rbOffset, MemoryUtil.memAddress(slice), byteLen); - downloads[localBufId] = downloaded; + downloads[localBufId] = slice; } out.put(input.getKey(), downloads[localBufId]); @@ -383,7 +734,9 @@ private Map resolveDownloads( @CallingThread(ThreadType.CLIENT) private void updateEMAs(Object2IntOpenHashMap dispatchCounts, long totalElapsedNs) { - int totalDispatches = dispatchCounts.values().intStream().sum(); + int totalDispatches = dispatchCounts.values() + .intStream() + .sum(); if (totalDispatches == 0) return; // Attribute wall-clock time equally across all dispatched kernels. @@ -406,36 +759,149 @@ private long getEstimateNs(KernelExecutor exec) { @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[0] = ema[1] == 0 ? sampleNs : (long) (EMA_ALPHA * sampleNs + (1 - EMA_ALPHA) * ema[0]); ema[1]++; } - private static class Allocator implements BufferAllocator { + 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); - /// The primary arena buffer, which will be closed after all plans have completed. - public final ArenaHardwareBuffer arena; + VkPipelineLayoutCreateInfo layoutInfo = VkPipelineLayoutCreateInfo.calloc(stack) + .sType$Default() + .pSetLayouts(stack.longs(descriptorSetLayout0, descriptorSetLayout1, descriptorSetLayout2)) + .pPushConstantRanges(pushConstantRange); - public Allocator(ArenaHardwareBuffer arena) { - this.arena = arena; + check(vkCreatePipelineLayout(getDevice(), layoutInfo, null, lp)); + pipelineLayout = lp.get(0); } + } - @Override - public GPUBuffer alloc(BufferDataType dataType, int lenX, int lenY, int lenZ) { - GPUBuffer subbuffer = arena.alloc(dataType, lenX, lenY, lenZ); + 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); + } + } - if (subbuffer != null) return subbuffer; + 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); + } + } - throw new IllegalStateException("Arena GPU buffer overflowed: something is allocating more memory than it planned for"); + 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()); - @Override - public void bindSSBO(int index) { - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, index, arena.getSsbo()); + 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()); } + } - @Override - public void unbindSSBO(int index) { - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, index, 0); + 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 index 06086142..2abb14e2 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelSubmission.java +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/KernelSubmission.java @@ -6,6 +6,6 @@ import com.github.bsideup.jabel.Desugar; @Desugar -public record KernelSubmission(K key, Map inputs) { +public record KernelSubmission (K key, Map inputs) { } 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 index 25331b52..b8f0551b 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/Noise2DKernelExecutor.java +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/Noise2DKernelExecutor.java @@ -1,118 +1,63 @@ package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; -import static org.lwjgl.opengl.GL20.glUseProgram; -import static org.lwjgl.opengl.GL43.glDispatchCompute; - import java.util.Map; import net.minecraft.world.ChunkCoordIntPair; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferAllocator; +import org.jetbrains.annotations.NotNull; + 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.ConstantHardwareBuffer; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.GPUBuffer; +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 implements KernelExecutor { +public class Noise2DKernelExecutor extends StandardKernelExecutor { + + @NotNull + private final NoiseSampler sampler; - private final int program; + public Noise2DKernelExecutor(@NotNull NoiseSampler sampler) { + this.sampler = sampler; + } - private final ConstantHardwareBuffer constants; + @Override + protected String generateKernel(KernelBuilder builder) { + builder.addParameter(BufferDataType.f32, "offsetX"); + builder.addParameter(BufferDataType.f32, "offsetY"); - public Noise2DKernelExecutor(NoiseSampler sampler) { - KernelBuilder builder = new KernelBuilder(); + builder.addOutputBuffer("noise", new BufferLayout(BufferDataType.f32, 16, 16)); - String result2d = sampler.compileKernel2D(builder, "gx", "gz"); + String result2d = sampler.compileKernel2D(builder, "gx", "gy"); - String code2 = - """ - #version 430 core + return """ + #version 460 - layout(local_size_x = 16, local_size_z = 16) in; + layout(local_size_x = 16, local_size_y = 16) in; - $uniform + layout(set = 0, binding = 0) readonly buffer Constants { uint constants[]; }; + layout(set = 1, binding = 0) buffer Arena { uint arena[]; }; - layout(std430, binding = 0) writeonly buffer Arena { float arena[]; }; - layout(std430, binding = 1) readonly buffer Uniforms { Noise2DUniform uniforms[]; }; - layout(std430, binding = 2) readonly buffer Constants { uint constants[]; }; + $pc $preamble void main() { - uint id = gl_WorkGroupID.x; - - uint x = gl_LocalInvocationID.x; - uint z = gl_LocalInvocationID.z; + uint x = gl_GlobalInvocationID.x; + uint y = gl_GlobalInvocationID.y; - int offsetX = uniforms[id].offsetX; - int offsetZ = uniforms[id].offsetZ; - uint outputOffset = uniforms[id].outputOffset; - - int gx = offsetX + int(x); - int gz = offsetZ + int(z); + float gx = GET_OFFSET_X + float(x); + float gy = GET_OFFSET_Y + float(y); $logic - arena[(z << 4) + x + outputOffset] = $result; + SET_NOISE((y << 4u) | x, $result); } - """; - - code2 = code2.replace("$preamble", builder.preamble.toString()) - .replace("$logic", builder.logic.toString()) - .replace("$result", result2d) - .replace("$uniform", Noise2DUniformGLStruct.SOURCE); - - this.program = KernelExecutor.createProgram(code2); - this.constants = builder.constants.finish(); - } - - @Override - public Map getOutputs( - ComputePlan plan, - KernelSubmissionToken submission, - ChunkCoordIntPair key, - Map inputs - ) { - return ImmutableMap.of("noise", plan.describeBuffer(submission, BufferDataType.f32, 16, 16)); + """.replace("$logic", builder.logic.toString()) + .replace("$result", result2d); } @Override - public KernelSubmissionResult[] submit(BufferAllocator alloc, KernelSubmission[] submissions) { - glUseProgram(this.program); - - Noise2DUniformPrimitiveBuffer uniforms = new Noise2DUniformPrimitiveBuffer(submissions.length); - - KernelSubmissionResult[] results = new KernelSubmissionResult[submissions.length]; - - uniforms.forEachFast((i, view) -> { - var key = submissions[i].key(); - - GPUBuffer output = alloc.alloc(BufferDataType.f32, 16, 16); - results[i] = new KernelSubmissionResult(ImmutableMap.of("noise", output)); - - view.setOffsetX(key.chunkXPos << 4); - view.setOffsetZ(key.chunkZPos << 4); - view.setOutputOffset(output.getBufferOffset() / 4); - - return true; - }); - - GPUBuffer uniformGPU = alloc.uniform(uniforms); - - alloc.bindSSBO(0); - uniformGPU.bind(1); - constants.bindSSBO(2); - - glDispatchCompute(submissions.length, 1, 1); - - alloc.unbindSSBO(0); - uniformGPU.unbind(1); - constants.unbindSSBO(2); - - glUseProgram(0); - - return results; + 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 index cefbd1f2..be55282b 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/PrimitiveBuffer.java +++ b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/PrimitiveBuffer.java @@ -30,7 +30,8 @@ public interface PrimitiveBuffer { /// 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. + /// 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. 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/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/WorkDomain.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/WorkDomain.java deleted file mode 100644 index 2be53215..00000000 --- a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/WorkDomain.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.cardinalstar.cubicchunks.api.worldgen.hwaccel; - -import com.cardinalstar.cubicchunks.util.IntFraction; - -public class WorkDomain { - - public int samplesX = 1, samplesY = 1, samplesZ = 1; - - public IntFraction scaleX = new IntFraction(), scaleY = new IntFraction(), scaleZ = new IntFraction(); - - public WorkDomain() { - } - - public WorkDomain(int samplesX, int samplesY, int samplesZ) { - this.samplesX = samplesX; - this.samplesY = samplesY; - this.samplesZ = samplesZ; - } - - public static WorkDomain chunk() { - return new WorkDomain(16, 1, 16); - } - - public static WorkDomain cube() { - return new WorkDomain(16, 16, 16); - } -} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/ArenaHardwareBuffer.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/ArenaHardwareBuffer.java deleted file mode 100644 index fe03e33b..00000000 --- a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/ArenaHardwareBuffer.java +++ /dev/null @@ -1,119 +0,0 @@ -package com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer; - -import static org.lwjgl.opengl.GL15.glBindBuffer; -import static org.lwjgl.opengl.GL15.glDeleteBuffers; -import static org.lwjgl.opengl.GL15.glGenBuffers; -import static org.lwjgl.opengl.GL15.glUnmapBuffer; -import static org.lwjgl.opengl.GL30.GL_MAP_FLUSH_EXPLICIT_BIT; -import static org.lwjgl.opengl.GL30.GL_MAP_INVALIDATE_BUFFER_BIT; -import static org.lwjgl.opengl.GL30.GL_MAP_READ_BIT; -import static org.lwjgl.opengl.GL30.GL_MAP_WRITE_BIT; -import static org.lwjgl.opengl.GL30.glFlushMappedBufferRange; -import static org.lwjgl.opengl.GL30.glMapBufferRange; -import static org.lwjgl.opengl.GL43.GL_SHADER_STORAGE_BUFFER; -import static org.lwjgl.opengl.GL44.GL_DYNAMIC_STORAGE_BIT; -import static org.lwjgl.opengl.GL44.GL_MAP_PERSISTENT_BIT; -import static org.lwjgl.opengl.GL44.glBufferStorage; - -import java.io.Closeable; -import java.nio.ByteBuffer; - -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelContext; -import com.cardinalstar.cubicchunks.util.MathUtil; -import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities; -import lombok.Getter; - -public class ArenaHardwareBuffer implements Closeable { - - @Getter - private int ssbo = 0; - @Getter - private int byteLen = 0; - - private int nextBufferOffset; - - @Getter - private boolean open = true; - - @Getter - private ByteBuffer mapped; - - public void download(ByteBuffer dest, int offset, int len) { - if (offset + len > byteLen) { - throw new IllegalArgumentException("Invalid parameters: offset=" + offset + ", len=" + len + ", byteLen=" + byteLen); - } - - if (len > dest.remaining()) { - throw new IllegalArgumentException("Invalid parameters: remaining=" + dest.remaining() + ", len=" + len); - } - - MemoryUtilities.memCopy(MemoryUtilities.memAddress(mapped) + offset, MemoryUtilities.memAddress(dest), len); - } - - public void upload(ByteBuffer src, int offset, int len) { - if (offset + len > byteLen) { - throw new IllegalArgumentException("Invalid parameters: offset=" + offset + ", len=" + len + ", byteLen=" + byteLen); - } - - if (len > src.remaining()) { - throw new IllegalArgumentException("Invalid parameters: remaining=" + src.remaining() + ", len=" + len); - } - - MemoryUtilities.memCopy(MemoryUtilities.memAddress(src), MemoryUtilities.memAddress(mapped) + offset, len); - - glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo); - glFlushMappedBufferRange(GL_SHADER_STORAGE_BUFFER, offset, len); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); - } - - public void reset() { - this.nextBufferOffset = 0; - } - - public void resize(int byteLen) { - if (this.ssbo != 0) { - close(); - } - - this.ssbo = glGenBuffers(); - - int storageFlags = GL_DYNAMIC_STORAGE_BIT | GL_MAP_READ_BIT | GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT; - int mapFlags = GL_MAP_READ_BIT | GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_FLUSH_EXPLICIT_BIT; - - glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo); - glBufferStorage(GL_SHADER_STORAGE_BUFFER, byteLen, storageFlags); - this.mapped = glMapBufferRange(GL_SHADER_STORAGE_BUFFER, 0, byteLen, mapFlags, null); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); - - this.open = true; - this.byteLen = byteLen; - } - - @Override - public void close() { - glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo); - glUnmapBuffer(GL_SHADER_STORAGE_BUFFER); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); - - glDeleteBuffers(this.ssbo); - - this.ssbo = 0; - this.open = false; - this.mapped = null; - this.byteLen = 0; - this.nextBufferOffset = 0; - } - - public HardwareSubBuffer alloc(BufferDataType dataType, int lenX, int lenY, int lenZ) { - int byteLen = dataType.width() * lenX * lenY * lenZ; - - if (this.nextBufferOffset + byteLen > this.byteLen) { - return null; - } - - int offset = this.nextBufferOffset; - this.nextBufferOffset = MathUtil.alignTo(this.nextBufferOffset + byteLen, KernelContext.getSSBOAlignment()); - - return new HardwareSubBuffer(this, offset, dataType, lenX, lenY, lenZ); - } -} 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 index 4e1669c4..cbd438c4 100644 --- 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 @@ -4,6 +4,8 @@ /// 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 index 11ae3548..9c7a0749 100644 --- 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 @@ -1,11 +1,5 @@ package com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer; -import java.nio.ByteBuffer; - -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.PrimitiveBuffer; -import com.cardinalstar.cubicchunks.util.MathUtil; -import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities; - public interface BufferAllocator { default GPUBuffer alloc(BufferDataType dataType, int lenX) { @@ -16,28 +10,9 @@ default GPUBuffer alloc(BufferDataType dataType, int lenX, int lenY) { return alloc(dataType, lenX, lenY, 1); } - default GPUBuffer uniform(PrimitiveBuffer buffer) { - int byteLen = buffer.getByteLength(); - int wordLen = MathUtil.ceilDiv(byteLen, 4); - - GPUBuffer gpu = alloc(BufferDataType.i32, wordLen); - - ByteBuffer temp = MemoryUtilities.memAlloc(byteLen); - - try { - buffer.upload(temp); - temp.flip(); - - gpu.upload(temp); - } finally { - MemoryUtilities.memFree(temp); - } - - return gpu; + 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); - - void bindSSBO(int index); - void unbindSSBO(int index); } 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 index 448aa1ba..e3474471 100644 --- 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 @@ -1,6 +1,7 @@ package com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer; public enum BufferDataType { + i32, u32, i64, @@ -14,4 +15,24 @@ public int width() { 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 index dcdc394b..adba3dcc 100644 --- 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 @@ -4,9 +4,8 @@ import com.github.bsideup.jabel.Desugar; @Desugar -public record BufferDescriptor( - KernelSubmissionToken submission, int bufferId, BufferDataType dataType, int lenX, int lenY, int lenZ -) { +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); @@ -17,14 +16,19 @@ public void assertLayout(BufferDataType dataType, int lenX, int lenY) { } 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); + 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() { 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/ConstantHardwareBuffer.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/ConstantHardwareBuffer.java deleted file mode 100644 index 1e6a785d..00000000 --- a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/ConstantHardwareBuffer.java +++ /dev/null @@ -1,114 +0,0 @@ -package com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer; - -import static org.lwjgl.opengl.GL15.GL_STATIC_DRAW; -import static org.lwjgl.opengl.GL15.glBindBuffer; -import static org.lwjgl.opengl.GL15.glBufferData; -import static org.lwjgl.opengl.GL15.glDeleteBuffers; -import static org.lwjgl.opengl.GL15.glGenBuffers; -import static org.lwjgl.opengl.GL30.glBindBufferBase; -import static org.lwjgl.opengl.GL43.GL_SHADER_STORAGE_BUFFER; - -import java.io.Closeable; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; - -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelContext; -import com.cardinalstar.cubicchunks.util.MathUtil; -import com.github.bsideup.jabel.Desugar; -import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities; -import lombok.Getter; - -public class ConstantHardwareBuffer implements Closeable { - - public final int ssbo; - private final String bufferName; - - @Getter - private boolean open = true; - private boolean finished = false; - - private final List ranges = new ArrayList<>(); - private int len; - - @Desugar - private record Range(int start, int size, long ptr) { } - - public ConstantHardwareBuffer(String bufferName) { - this.ssbo = glGenBuffers(); - this.bufferName = bufferName; - } - - public BufferAccessor append(BufferDataType dataType, ByteBuffer data) { - if (dataType.width() != 4) throw new IllegalArgumentException("Constant buffer can only store data types whose byte length is exactly 4 bytes"); - - int size = data.remaining(); - - long ptr = MemoryUtilities.nmemAlloc(data.remaining()); - MemoryUtilities.memByteBuffer(ptr, size).put(data); - - int offset = len; - - ranges.add(new Range(offset, size, ptr)); - - len = MathUtil.alignTo(len + size, KernelContext.getSSBOAlignment()); - - return index -> { - String base = String.format("%s[(%s) + %d]", this.bufferName, index, offset / 4); - - return switch (dataType) { - case i32 -> "(int(" + base + "))"; - case u32 -> "(" + base + ")"; - case f32 -> "(uintBitsToFloat(" + base + "))"; - default -> throw new AssertionError(); - }; - }; - } - - @Override - public void close() { - for (var range : ranges) { - MemoryUtilities.nmemFree(range.ptr); - } - - ranges.clear(); - - this.open = false; - glDeleteBuffers(this.ssbo); - } - - public ConstantHardwareBuffer finish() { - if (finished) throw new IllegalStateException("Cannot finish ConstantHardwareBuffer twice"); - finished = true; - - ByteBuffer data = MemoryUtilities.memAlloc(len); - long dataPtr = MemoryUtilities.memAddress(data); - - for (var range : ranges) { - MemoryUtilities.memCopy(range.ptr, dataPtr, range.size); - MemoryUtilities.nmemFree(range.ptr); - } - - ranges.clear(); - - glBindBuffer(GL_SHADER_STORAGE_BUFFER, this.ssbo); - glBufferData(GL_SHADER_STORAGE_BUFFER, data, GL_STATIC_DRAW); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); - - MemoryUtilities.memFree(data); - - return this; - } - - public void bindSSBO(int index) { - if (!finished) throw new IllegalStateException("Cannot bind unfinished ConstantHardwareBuffer"); - - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, index, this.ssbo); - } - - public void unbindSSBO(int index) { - if (!finished) throw new IllegalStateException("Cannot bind unfinished ConstantHardwareBuffer"); - - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, index, 0); - } -} 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 index f3d33614..1a04132b 100644 --- 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 @@ -1,102 +1,18 @@ package com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer; -import static org.lwjgl.opengl.GL11.glGetError; -import static org.lwjgl.opengl.GL15.glBindBuffer; -import static org.lwjgl.opengl.GL15.glUnmapBuffer; -import static org.lwjgl.opengl.GL30.GL_MAP_READ_BIT; -import static org.lwjgl.opengl.GL30.GL_MAP_WRITE_BIT; -import static org.lwjgl.opengl.GL30.glMapBufferRange; -import static org.lwjgl.opengl.GL31.GL_COPY_READ_BUFFER; -import static org.lwjgl.opengl.GL31.GL_COPY_WRITE_BUFFER; -import static org.lwjgl.opengl.GL31.glCopyBufferSubData; -import static org.lwjgl.opengl.GL43.GL_SHADER_STORAGE_BUFFER; - -import java.io.Closeable; -import java.nio.ByteBuffer; - -import org.lwjgl.util.glu.GLU; - -import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities; - -public interface GPUBuffer extends Closeable { +public interface GPUBuffer { BufferDataType getDataType(); - int getBufferId(); int getBufferOffset(); + default int getBufferLength() { return getDataType().width() * getLenX() * getLenY() * getLenZ(); } int getLenX(); - int getLenY(); - int getLenZ(); - - void bind(int index); - void unbind(int index); - - default void copyTo(GPUBuffer dest) { - if (this.getBufferLength() != dest.getBufferLength()) { - throw new UnsupportedOperationException("Both buffers must be the same length when copying"); - } - - glBindBuffer(GL_COPY_READ_BUFFER, this.getBufferId()); - glBindBuffer(GL_COPY_WRITE_BUFFER, dest.getBufferId()); - - glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, this.getBufferOffset(), dest.getBufferOffset(), this.getBufferLength()); - - glBindBuffer(GL_COPY_READ_BUFFER, 0); - glBindBuffer(GL_COPY_WRITE_BUFFER, 0); - } - - default void download(ByteBuffer dest) { - if (this.getBufferLength() != dest.remaining()) { - throw new UnsupportedOperationException("Both buffers must be the same length when copying"); - } - - glBindBuffer(GL_SHADER_STORAGE_BUFFER, getBufferId()); - ByteBuffer mapped = glMapBufferRange(GL_SHADER_STORAGE_BUFFER, getBufferOffset(), getBufferLength(), GL_MAP_READ_BIT, null); - - if (mapped == null) { - int error = glGetError(); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); - - throw new IllegalStateException("Could not map buffer: " + GLU.gluErrorString(error)); - } - - MemoryUtilities.memCopy(mapped, dest); - dest.position(dest.position() + this.getBufferLength()); - - glUnmapBuffer(GL_SHADER_STORAGE_BUFFER); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); - } - - default void upload(ByteBuffer data) { - if (this.getBufferLength() != data.remaining()) { - throw new UnsupportedOperationException("Both buffers must be the same length when copying"); - } - - glBindBuffer(GL_SHADER_STORAGE_BUFFER, getBufferId()); - ByteBuffer mapped = glMapBufferRange(GL_SHADER_STORAGE_BUFFER, getBufferOffset(), getBufferLength(), GL_MAP_WRITE_BIT, null); - - if (mapped == null) { - int error = glGetError(); - - glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); - - throw new IllegalStateException("Could not map buffer: " + GLU.gluErrorString(error)); - } - - MemoryUtilities.memCopy(data, mapped); - data.position(data.position() + this.getBufferLength()); - - glUnmapBuffer(GL_SHADER_STORAGE_BUFFER); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); - } - - @Override - void close(); + int getLenY(); - boolean shouldBeCached(); + int getLenZ(); } diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/HardwareBuffer.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/HardwareBuffer.java deleted file mode 100644 index 7020099f..00000000 --- a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/HardwareBuffer.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer; - -import static org.lwjgl.opengl.GL15.GL_DYNAMIC_DRAW; -import static org.lwjgl.opengl.GL15.glBindBuffer; -import static org.lwjgl.opengl.GL15.glBufferData; -import static org.lwjgl.opengl.GL15.glDeleteBuffers; -import static org.lwjgl.opengl.GL15.glGenBuffers; -import static org.lwjgl.opengl.GL30.glBindBufferBase; -import static org.lwjgl.opengl.GL43.GL_SHADER_STORAGE_BUFFER; - -import java.io.Closeable; - -import lombok.Getter; -import lombok.Setter; - -public class HardwareBuffer implements Closeable, GPUBuffer { - - public final int ssbo; - - @Getter - public final BufferDataType dataType; - @Getter - public final int lenX, lenY, lenZ; - - private boolean open = true; - @Setter - private boolean shouldBeCached; - - public HardwareBuffer(int ssbo, BufferDataType dataType, int lenX, int lenY, int lenZ) { - this.ssbo = ssbo; - this.dataType = dataType; - this.lenX = lenX; - this.lenY = lenY; - this.lenZ = lenZ; - } - - @Override - public int getBufferId() { - return ssbo; - } - - @Override - public int getBufferOffset() { - return 0; - } - - @Override - public void bind(int index) { - if (!open) { - throw new IllegalStateException("Cannot bind closed HardwareBuffer"); - } - - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, index, this.ssbo); - } - - @Override - public void unbind(int index) { - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, index, 0); - } - - public static HardwareBuffer alloc(BufferDataType dataType, int lenX, int lenY, int lenZ) { - int ssbo = glGenBuffers(); - - glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo); - glBufferData(GL_SHADER_STORAGE_BUFFER, (long) dataType.width() * lenX * lenY * lenZ, GL_DYNAMIC_DRAW); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); - - return new HardwareBuffer(ssbo, dataType, lenX, lenY, lenZ); - } - - @Override - public void close() { - this.open = false; - glDeleteBuffers(this.ssbo); - } - - @Override - public boolean shouldBeCached() { - return shouldBeCached; - } -} diff --git a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/HardwareSubBuffer.java b/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/HardwareSubBuffer.java deleted file mode 100644 index 73e4eeea..00000000 --- a/src/main/java/com/cardinalstar/cubicchunks/api/worldgen/hwaccel/buffer/HardwareSubBuffer.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer; - -import static org.lwjgl.opengl.GL30.glBindBufferBase; -import static org.lwjgl.opengl.GL30.glBindBufferRange; -import static org.lwjgl.opengl.GL43.GL_SHADER_STORAGE_BUFFER; - -import java.nio.ByteBuffer; - -import lombok.Getter; - -public class HardwareSubBuffer implements GPUBuffer { - - private final ArenaHardwareBuffer arena; - public final int ssbo; - - public final int ssboOffset; - - @Getter - public final BufferDataType dataType; - @Getter - public final int lenX, lenY, lenZ; - - public HardwareSubBuffer(ArenaHardwareBuffer arena, int ssboOffset, BufferDataType dataType, int lenX, int lenY, int lenZ) { - this.arena = arena; - this.ssbo = arena.getSsbo(); - this.ssboOffset = ssboOffset; - this.dataType = dataType; - this.lenX = lenX; - this.lenY = lenY; - this.lenZ = lenZ; - } - - @Override - public int getBufferId() { - return ssbo; - } - - @Override - public int getBufferOffset() { - return ssboOffset; - } - - @Override - public void bind(int index) { - if (!arena.isOpen()) { - throw new IllegalStateException("Cannot bind HardwareSubBuffer when its arena buffer has been closed"); - } - - glBindBufferRange(GL_SHADER_STORAGE_BUFFER, index, this.ssbo, this.ssboOffset, this.getBufferLength()); - } - - @Override - public void unbind(int index) { - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, index, 0); - } - - @Override - public void close() { - // do nothing - } - - @Override - public boolean shouldBeCached() { - return false; - } - - @Override - public void download(ByteBuffer dest) { - this.arena.download(dest, getBufferOffset(), getBufferLength()); - } - - @Override - public void upload(ByteBuffer data) { - this.arena.upload(data, getBufferOffset(), getBufferLength()); - } -} 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/lighting/FirstLightProcessor.java b/src/main/java/com/cardinalstar/cubicchunks/lighting/FirstLightProcessor.java index 892ee758..765f262e 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/lighting/FirstLightProcessor.java +++ b/src/main/java/com/cardinalstar/cubicchunks/lighting/FirstLightProcessor.java @@ -40,6 +40,7 @@ import com.cardinalstar.cubicchunks.util.MathUtil; import com.cardinalstar.cubicchunks.world.core.IColumnInternal; import com.cardinalstar.cubicchunks.world.cube.Cube; + import it.unimi.dsi.fastutil.ints.IntIntMutablePair; /** @@ -168,11 +169,13 @@ public void diffuseSkylight(ICube cube) { // 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 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); + int opacityTopBlock = column.getOpacityIndex() + .getTopBlockY(lX, lZ); minMax.left(opacityTopBlock); minMax.right(stagingTopBlock); @@ -187,8 +190,7 @@ public void diffuseSkylight(ICube cube) { } // Iterate over all affected cubes. - Iterable cubes = column - .getLoadedCubes(blockToCube(minMinHeight), blockToCube(maxMaxHeight)); + Iterable cubes = column.getLoadedCubes(blockToCube(minMinHeight), blockToCube(maxMaxHeight)); for (Cube affectedCube : cubes) { int bY_Affected = affectedCube.getY() << 4; @@ -199,7 +201,7 @@ public void diffuseSkylight(ICube cube) { int minBlockY = minBlockYArr[(lZ << 4) | lX]; int maxBlockY = maxBlockYArr[(lZ << 4) | lX]; - // is below the existing top of the block + // is below the existing top of the block if (minBlockY > maxBlockY) { continue; } @@ -214,13 +216,7 @@ public void diffuseSkylight(ICube cube) { } // Update the block column in this cube. - diffuseSkylightInBlockColumn( - lm, - affectedCube, - lX + bX, - lZ + bZ, - minBlockY, - maxBlockY); + diffuseSkylightInBlockColumn(lm, affectedCube, lX + bX, lZ + bZ, minBlockY, maxBlockY); } } } @@ -238,8 +234,8 @@ public void diffuseSkylight(ICube cube) { * @param minBlockY the lower bound of the section to be updated * @param maxBlockY the upper bound of the section to be updated */ - private void diffuseSkylightInBlockColumn(ILightingManager lm, ICube cube, int posX, 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()); @@ -263,7 +259,8 @@ private void diffuseSkylightInBlockColumn(ILightingManager lm, ICube cube, int p 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. + // 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) { diff --git a/src/main/java/com/cardinalstar/cubicchunks/lighting/LightingManager.java b/src/main/java/com/cardinalstar/cubicchunks/lighting/LightingManager.java index a13bf347..f1a27f88 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/lighting/LightingManager.java +++ b/src/main/java/com/cardinalstar/cubicchunks/lighting/LightingManager.java @@ -186,8 +186,10 @@ public void onTrackCubeSurface(ICube cube) { if (!world.isRemote) { CubicPlayerManager playerManager = getPlayerManager(); if (playerManager != null) { - int minX = cube.getCoords().getMinBlockX(); - int minZ = cube.getCoords().getMinBlockZ(); + 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); 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 d870e801..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(x, 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/early/common/MixinBlock_Lighting.java b/src/main/java/com/cardinalstar/cubicchunks/mixin/early/common/MixinBlock_Lighting.java index 445ae7ae..8d02ed9b 100644 --- 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 @@ -21,7 +21,11 @@ public class MixinBlock_Lighting implements BlockExt_Lighting { 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;")) + @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 index 3871eb16..c0e94f7c 100644 --- 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 @@ -11,11 +11,9 @@ @Mixin(ExtendedBlockStorage.class) public abstract class MixinEBSID_EID implements EBSIDAccessor { - @Shadow(remap = false) @Dynamic public abstract int eid$getID(int x, int y, int z); - @Shadow(remap = false) @Dynamic public abstract void eid$setID(int x, int y, int z, int id); 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 index 4764c3ab..7f186904 100644 --- 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 @@ -36,7 +36,7 @@ public int getBlockID(int x, int y, int z) { @Override public void setBlockID(int x, int y, int z, int id, boolean tickRandomly) { - this.blockLSBArray[y << 8 | z << 4 | x] = (byte)(id & 255); + this.blockLSBArray[y << 8 | z << 4 | x] = (byte) (id & 255); if (id > 255) { if (this.blockMSBArray == null) { 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 57fe8eb4..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 @@ -521,9 +521,9 @@ 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" - )) + @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; diff --git a/src/main/java/com/cardinalstar/cubicchunks/mixin/ext/EBSIDAccessor.java b/src/main/java/com/cardinalstar/cubicchunks/mixin/ext/EBSIDAccessor.java index 67d45521..d5e34aa7 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/mixin/ext/EBSIDAccessor.java +++ b/src/main/java/com/cardinalstar/cubicchunks/mixin/ext/EBSIDAccessor.java @@ -3,6 +3,7 @@ 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/network/PacketEncoderCube.java b/src/main/java/com/cardinalstar/cubicchunks/network/PacketEncoderCube.java index 2fef1f59..670ccae1 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/network/PacketEncoderCube.java +++ b/src/main/java/com/cardinalstar/cubicchunks/network/PacketEncoderCube.java @@ -25,6 +25,7 @@ import javax.annotation.ParametersAreNonnullByDefault; +import net.jpountz.lz4.LZ4Factory; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.tileentity.TileEntity; import net.minecraft.world.World; @@ -37,9 +38,9 @@ import com.cardinalstar.cubicchunks.util.CubeStatusVisualizer.CubeStatus; import com.cardinalstar.cubicchunks.world.cube.Cube; import com.github.bsideup.jabel.Desugar; + import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; -import net.jpountz.lz4.LZ4Factory; @ParametersAreNonnullByDefault public class PacketEncoderCube extends CCPacketEncoder { @@ -91,7 +92,10 @@ public void writePacket(CCPacketBuffer buffer, PacketCube packet) { buffer.writeCubePos(packet.cubePos); buffer.writeVarIntToBuffer(packet.data.length); - buffer.writeByteArray(LZ4Factory.fastestInstance().fastCompressor().compress(packet.data)); + buffer.writeByteArray( + LZ4Factory.fastestInstance() + .fastCompressor() + .compress(packet.data)); buffer.writeList(packet.tileEntityTags, CCPacketBuffer::writeCompoundTag); } @@ -103,7 +107,9 @@ public PacketCube readPacket(CCPacketBuffer buf) { byte[] decompressed = new byte[buf.readVarIntFromBuffer()]; byte[] data = buf.readByteArray(); - LZ4Factory.fastestInstance().fastDecompressor().decompress(data, decompressed); + LZ4Factory.fastestInstance() + .fastDecompressor() + .decompress(data, decompressed); List tileEntityTags = buf.readList(CCPacketBuffer::readCompoundTag); diff --git a/src/main/java/com/cardinalstar/cubicchunks/network/PacketEncoderUpdateVisualizedBoxes.java b/src/main/java/com/cardinalstar/cubicchunks/network/PacketEncoderUpdateVisualizedBoxes.java index 142aca41..26a48caf 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/network/PacketEncoderUpdateVisualizedBoxes.java +++ b/src/main/java/com/cardinalstar/cubicchunks/network/PacketEncoderUpdateVisualizedBoxes.java @@ -14,7 +14,8 @@ public class PacketEncoderUpdateVisualizedBoxes extends CCPacketEncoder { @Desugar - public record PacketUpdateVisualizedBoxes(long timeout, boolean append, boolean disableDepth, List boxes) implements CCPacket { + public record PacketUpdateVisualizedBoxes(long timeout, boolean append, boolean disableDepth, + List boxes) implements CCPacket { @Override public byte getPacketID() { diff --git a/src/main/java/com/cardinalstar/cubicchunks/server/CubeProviderServer.java b/src/main/java/com/cardinalstar/cubicchunks/server/CubeProviderServer.java index 0f5d2ab3..b0ca6371 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/server/CubeProviderServer.java +++ b/src/main/java/com/cardinalstar/cubicchunks/server/CubeProviderServer.java @@ -71,6 +71,7 @@ import com.cardinalstar.cubicchunks.world.savedata.WorldFormatSavedData; import com.google.common.collect.ListMultimap; 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; @@ -337,9 +338,7 @@ public void tick() { acc += post - pre; if (counter++ == 20) { - CubicChunks.LOGGER.info( - "Worldgen {}ms", - (acc / 1e6) / counter); + CubicChunks.LOGGER.info("Worldgen {}ms", (acc / 1e6) / counter); acc = 0; counter = 0; diff --git a/src/main/java/com/cardinalstar/cubicchunks/server/WorldSyncStateMachine.java b/src/main/java/com/cardinalstar/cubicchunks/server/WorldSyncStateMachine.java index 34cb5b9d..2bff1a8b 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/server/WorldSyncStateMachine.java +++ b/src/main/java/com/cardinalstar/cubicchunks/server/WorldSyncStateMachine.java @@ -168,7 +168,8 @@ public void onColumnHeightMarkedDirty(int x, int z) { } public void onSurfaceTracked(Cube cube) { - dirtyHeightCols.computeIfAbsent(cube.getX(), cube.getZ(), (x1, z1) -> new BooleanArray2D(16, 16)).set(0, 256, true); + 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) { 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 282537ed..b4bf5911 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/server/chunkio/CCNBTUtils.java +++ b/src/main/java/com/cardinalstar/cubicchunks/server/chunkio/CCNBTUtils.java @@ -68,7 +68,8 @@ private static int getTagSizeEstimate(NBTBase tag) { return 5 + ((NBTTagIntArray) tag).func_150302_c().length * 4; } case NBT.TAG_STRING -> { - return 1 + ((NBTTagString) tag).func_150285_a_().length() * 2; + return 1 + ((NBTTagString) tag).func_150285_a_() + .length() * 2; } case NBT.TAG_LIST -> { var list = ((AccessorNBTTagList) tag).getTagList(); @@ -112,7 +113,7 @@ public GZIPOutputStream2(ByteArrayOutputStream nos) throws IOException { @Override public void write(int b) throws IOException { - pooled[0] = (byte)(b & 0xff); + 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 fcdff496..ee77148a 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/server/chunkio/CubeLoaderServer.java +++ b/src/main/java/com/cardinalstar/cubicchunks/server/chunkio/CubeLoaderServer.java @@ -462,11 +462,12 @@ 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); + CubicChunks.LOGGER.warn( + "addColumn tried to replace column at {},{}! (in-memory source: {}, attempted replacement source: {})", + column.xPosition, + column.zPosition, + info.source, + ObjectSource.Added); return; } @@ -486,11 +487,13 @@ 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); + 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; } @@ -514,11 +517,12 @@ private void handleSideEffects(GenerationResult result, boolean doColumns, bo ColumnInfo info = columns.get(column.xPosition, column.zPosition); if (info != null && info.column != null) { - 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); + 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; } @@ -539,11 +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 tried to replace cube at {},{},{}! (in-memory source: {}, attempted replacement source: {})", - cube.getX(), cube.getY(), cube.getZ(), - info.source, - ObjectSource.GeneratedSideEffect); + 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; } @@ -896,7 +902,8 @@ private boolean generate(CubeInitLevel requestedInitLevel) { if (!generated) return false; if (!isInitedTo(CubeInitLevel.Populated)) { - // If this cube hasn't been populated at all, populate it. This generates any required cubes recursively. + // If this cube hasn't been populated at all, populate it. This generates any required cubes + // recursively. generator.populate(cube); } diff --git a/src/main/java/com/cardinalstar/cubicchunks/util/CubeStatusVisualizer.java b/src/main/java/com/cardinalstar/cubicchunks/util/CubeStatusVisualizer.java index 25bc6da4..1856616f 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/util/CubeStatusVisualizer.java +++ b/src/main/java/com/cardinalstar/cubicchunks/util/CubeStatusVisualizer.java @@ -24,6 +24,7 @@ public class CubeStatusVisualizer { public enum CubeStatus { + Enqueued(RGBColor.fromRGBA(0x64292832)), None(RGBColor.fromRGBA(0x64326432)), Generated(RGBColor.fromRGBA(0x32C83232)), @@ -57,7 +58,8 @@ public static void sync(ServerTickEvent event) { if (!CubicChunksConfig.enableChunkStatusDebugging) { if (wasSent) { wasSent = false; - for (EntityPlayerMP player : MinecraftServer.getServer().getConfigurationManager().playerEntityList) { + for (EntityPlayerMP player : MinecraftServer.getServer() + .getConfigurationManager().playerEntityList) { BoxVisualizer.sendBoxes(player, Duration.ofMinutes(0), new ArrayList<>(), true); } } @@ -71,17 +73,20 @@ public static void sync(ServerTickEvent event) { 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 - ); + 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) { + for (EntityPlayerMP player : MinecraftServer.getServer() + .getConfigurationManager().playerEntityList) { BoxVisualizer.sendBoxes(player, Duration.ofMinutes(5), boxes, false); } } 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/JavaUtils.java b/src/main/java/com/cardinalstar/cubicchunks/util/JavaUtils.java index a141406e..2181fd5e 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/util/JavaUtils.java +++ b/src/main/java/com/cardinalstar/cubicchunks/util/JavaUtils.java @@ -18,7 +18,9 @@ public static void onSpinWait() { /// 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"); + + 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/boxvisualizer/BoxVisualizer.java b/src/main/java/com/cardinalstar/cubicchunks/util/boxvisualizer/BoxVisualizer.java index dcc2bd70..6826935b 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/util/boxvisualizer/BoxVisualizer.java +++ b/src/main/java/com/cardinalstar/cubicchunks/util/boxvisualizer/BoxVisualizer.java @@ -11,17 +11,21 @@ public class BoxVisualizer { - private static final int MAX_PACKET_SIZE = 3200, - BYTES_PER_BOX = 4 + 4 * 3 + 4 * 3, + 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) { + 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)); + 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 index f747aff7..0cbf59fe 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/util/boxvisualizer/QuadSorter.java +++ b/src/main/java/com/cardinalstar/cubicchunks/util/boxvisualizer/QuadSorter.java @@ -7,15 +7,17 @@ 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) { + 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; + int quadCount = bufferLen / quadStride / 4; float[] distanceArray = new float[quadCount]; int[] indicesArray = new int[quadCount]; @@ -71,7 +73,8 @@ public static void rearrangeQuads(FloatBuffer buffer, int[] indicesArray, int st } } - private static float getDistanceSqSFP(FloatBuffer buffer, float xCenter, float yCenter, float zCenter, int stride, int start) { + 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); @@ -87,10 +90,10 @@ private static float getDistanceSqSFP(FloatBuffer buffer, float xCenter, float y 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); + // 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; @@ -112,10 +115,10 @@ private static float getDistanceSqSFP(FloatBuffer buffer, float xCenter, float y 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); + // 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 index 5e1950ac..f8a950e3 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/util/boxvisualizer/VisualizedBox.java +++ b/src/main/java/com/cardinalstar/cubicchunks/util/boxvisualizer/VisualizedBox.java @@ -21,8 +21,12 @@ public VisualizedBox(int rgba, AxisAlignedBB 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); + bounds.minX - amount, + bounds.minY - amount, + bounds.minZ - amount, + bounds.maxX + amount, + bounds.maxY + amount, + bounds.maxZ + amount); return this; } diff --git a/src/main/java/com/cardinalstar/cubicchunks/util/boxvisualizer/VisualizedBoxRenderer.java b/src/main/java/com/cardinalstar/cubicchunks/util/boxvisualizer/VisualizedBoxRenderer.java index 0e6e9798..1d2fee49 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/util/boxvisualizer/VisualizedBoxRenderer.java +++ b/src/main/java/com/cardinalstar/cubicchunks/util/boxvisualizer/VisualizedBoxRenderer.java @@ -15,6 +15,7 @@ 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; @@ -60,7 +61,8 @@ public static void renderBoxes(RenderWorldLastEvent event) { tessellator.startDrawingQuads(); for (VisualizedBox box : boxes) { - tessellator.setColorRGBA(box.color.getRed(), box.color.getGreen(), box.color.getBlue(), box.color.getAlpha()); + tessellator + .setColorRGBA(box.color.getRed(), box.color.getGreen(), box.color.getBlue(), box.color.getAlpha()); drawBox(tessellator, xd, yd, zd, box.bounds); } @@ -69,7 +71,13 @@ public static void renderBoxes(RenderWorldLastEvent event) { ByteBuffer quads = TessellatorManager.stopCapturingToBuffer(DefaultVertexFormat.POSITION_COLOR_TEXTURE); - QuadSorter.sortStandardFormat(DefaultVertexFormat.POSITION_COLOR_TEXTURE, quads.asFloatBuffer(), quads.capacity(), (float) xd, (float) yd, (float) zd); + 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); @@ -87,13 +95,7 @@ public static void renderBoxes(RenderWorldLastEvent event) { GL11.glPopAttrib(); } - private static void drawBox( - Tessellator tes, - double eyeX, - double eyeY, - double eyeZ, - AxisAlignedBB bounds - ) { + 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; diff --git a/src/main/java/com/cardinalstar/cubicchunks/world/layer/LayeredWorld.java b/src/main/java/com/cardinalstar/cubicchunks/world/layer/LayeredWorld.java index 61fcc18e..b6f8fb9a 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/world/layer/LayeredWorld.java +++ b/src/main/java/com/cardinalstar/cubicchunks/world/layer/LayeredWorld.java @@ -23,7 +23,12 @@ 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()); + super( + new DummySaveHandler(), + "DUMMY_DIMENSION", + null, + new WorldSettings(new WorldInfo(new NBTTagCompound())), + new Profiler()); this.next = next; this.rand = new XSTR(); @@ -45,44 +50,44 @@ public boolean setBlock(int aX, int aY, int aZ, Block aBlock, int aMeta, int aFl 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 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 + // 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() { diff --git a/src/main/java/com/cardinalstar/cubicchunks/world/layer/WorldLayer.java b/src/main/java/com/cardinalstar/cubicchunks/world/layer/WorldLayer.java index fabbe9d5..66ab9d4b 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/world/layer/WorldLayer.java +++ b/src/main/java/com/cardinalstar/cubicchunks/world/layer/WorldLayer.java @@ -11,6 +11,7 @@ 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 { @@ -40,6 +41,7 @@ public boolean getBlockMeta(int x, int y, int z, MutableInt meta) { } private static class PseudoChunk { + public final int[] heightmap = new int[16 * 16]; public final BooleanArray2D presencce = new BooleanArray2D(16, 16); @@ -47,6 +49,7 @@ private static class PseudoChunk { } 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/noise/NormalizedSampler.java b/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/NormalizedSampler.java index 4bee9984..107da979 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/NormalizedSampler.java +++ b/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/NormalizedSampler.java @@ -55,10 +55,20 @@ 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"); + 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; } @@ -67,10 +77,20 @@ public String compileKernel3D(KernelBuilder builder, String x, String y, String 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"); + 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 6dc14275..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 @@ -69,22 +69,32 @@ public double sample(double x, double y, double z) { @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"); + 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)); + 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(value) + .append(" * ") + .append((float) amplitude) + .append("f;\n"); } - builder.logic.append(" ").append(result).append(" *= ").append((float) this.norm).append("f;\n"); + builder.logic.append(" ") + .append(result) + .append(" *= ") + .append((float) this.norm) + .append("f;\n"); return result; } @@ -92,7 +102,9 @@ public String compileKernel2D(KernelBuilder builder, String x, String y) { @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"); + builder.logic.append(" float ") + .append(result) + .append(" = 0.0f;\n"); for (int i = 0, octavesLength = octaves.length; i < octavesLength; i++) { NoiseSampler sampler = octaves[i]; @@ -105,10 +117,20 @@ public String compileKernel3D(KernelBuilder builder, String x, String y, String 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(value) + .append(" * ") + .append((float) amplitude) + .append("f;\n"); } - builder.logic.append(" ").append(result).append(" *= ").append((float) this.norm).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/ScaledSampler.java b/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/ScaledSampler.java index 0fed4da5..1b80b01e 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/ScaledSampler.java +++ b/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/ScaledSampler.java @@ -34,11 +34,16 @@ public double sample(double x, double y, double z) { @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)); + 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)); + 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/SimplexSampler.java b/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/SimplexSampler.java index b097b8aa..ad29456f 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/SimplexSampler.java +++ b/src/main/java/com/cardinalstar/cubicchunks/world/worldgen/noise/SimplexSampler.java @@ -1,19 +1,18 @@ package com.cardinalstar.cubicchunks.world.worldgen.noise; -import java.nio.ByteBuffer; import java.util.Random; import net.minecraft.util.MathHelper; import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelBuilder; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferDataType; +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; @@ -70,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; @@ -192,115 +184,72 @@ public double sample(double x, double y, double z) { public String compileKernel2D(KernelBuilder builder, String x, String y) { String funcName = builder.createName("simplex2d"); - ByteBuffer permBytes = ByteBuffer.allocateDirect(permutations.length * 4); - permBytes.asIntBuffer().put(permutations); + 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); - var perms = builder.constants.append(BufferDataType.i32, permBytes); + 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)); - float skew = (float) SKEW_FACTOR_2D; - float unskew = (float) UNSKEW_FACTOR_2D; + 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); - // Gradients: GRADIENTS[][0] and [][1] for indices 0-11 - builder.preamble - .append("float ").append(funcName).append("(float px, float py) {\n") - .append(" const int GX[12] = int[12](1,-1,1,-1,1,-1,1,-1,0,0,0,0);\n") - .append(" const int GY[12] = int[12](1,1,-1,-1,0,0,0,0,1,-1,1,-1);\n") - .append(" float d = (px + py) * ").append(skew).append("f;\n") - .append(" int i = int(floor(px + d));\n") - .append(" int j = int(floor(py + d));\n") - .append(" float e = float(i + j) * ").append(unskew).append("f;\n") - .append(" float h = px - (float(i) - e);\n") - .append(" float k = py - (float(j) - e);\n") - .append(" int n = h > k ? 1 : 0;\n") - .append(" int o = h > k ? 0 : 1;\n") - .append(" float p = h - float(n) + ").append(unskew).append("f;\n") - .append(" float q = k - float(o) + ").append(unskew).append("f;\n") - .append(" float r = h - 1.0f + 2.0f * ").append(unskew).append("f;\n") - .append(" float s = k - 1.0f + 2.0f * ").append(unskew).append("f;\n") - .append(" int t = i & 255;\n") - .append(" int u = j & 255;\n") - .append(" int v = ").append(perms.access("(t + " + perms.access("u") + ") & 255")).append(" % 12;\n") - .append(" int w = ").append(perms.access("(t + n + " + perms.access("(u + o) & 255") + ") & 255")).append(" % 12;\n") - .append(" int z = ").append(perms.access("(t + 1 + " + perms.access("(u + 1) & 255") + ") & 255")).append(" % 12;\n") - .append(" float t0 = max(0.0f, 0.5f - h*h - k*k);\n") - .append(" float n0 = t0*t0*t0*t0 * (float(GX[v])*h + float(GY[v])*k);\n") - .append(" float t1 = max(0.0f, 0.5f - p*p - q*q);\n") - .append(" float n1 = t1*t1*t1*t1 * (float(GX[w])*p + float(GY[w])*q);\n") - .append(" float t2 = max(0.0f, 0.5f - r*r - s*s);\n") - .append(" float n2 = t2*t2*t2*t2 * (float(GX[z])*r + float(GY[z])*s);\n") - .append(" return 70.0f * (n0 + n1 + n2);\n") - .append("}\n"); + 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"); + 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) { - String funcName = builder.createName("simplex3d"); - - ByteBuffer permBytes = ByteBuffer.allocateDirect(permutations.length * 4); - permBytes.asIntBuffer().put(permutations); - - var perms = builder.constants.append(BufferDataType.i32, permBytes); - - // Gradients: GRADIENTS[][0..2] for indices 0-11 - builder.preamble - .append("float ").append(funcName).append("(float px, float py, float pz) {\n") - .append(" const int GX[12] = int[12](1,-1,1,-1,1,-1,1,-1,0,0,0,0);\n") - .append(" const int GY[12] = int[12](1,1,-1,-1,0,0,0,0,1,-1,1,-1);\n") - .append(" const int GZ[12] = int[12](0,0,0,0,1,1,-1,-1,1,1,-1,-1);\n") - .append(" float sk = (px + py + pz) * 0.3333333333f;\n") - .append(" int i = int(floor(px + sk));\n") - .append(" int j = int(floor(py + sk));\n") - .append(" int k = int(floor(pz + sk));\n") - .append(" float g = float(i + j + k) * 0.1666666667f;\n") - .append(" float x0 = px - (float(i) - g);\n") - .append(" float y0 = py - (float(j) - g);\n") - .append(" float z0 = pz - (float(k) - g);\n") - // 6-way vertex ordering (mirrors the Java if/else chain exactly) - .append(" int i1, j1, k1, i2, j2, k2;\n") - .append(" if (x0 >= y0) {\n") - .append(" if (y0 >= z0) { i1=1;j1=0;k1=0; i2=1;j2=1;k2=0; }\n") - .append(" else if (x0 >= z0) { i1=1;j1=0;k1=0; i2=1;j2=0;k2=1; }\n") - .append(" else { i1=0;j1=0;k1=1; i2=1;j2=0;k2=1; }\n") - .append(" } else {\n") - .append(" if (y0 < z0) { i1=0;j1=0;k1=1; i2=0;j2=1;k2=1; }\n") - .append(" else if (x0 < z0) { i1=0;j1=1;k1=0; i2=0;j2=1;k2=1; }\n") - .append(" else { i1=0;j1=1;k1=0; i2=1;j2=1;k2=0; }\n") - .append(" }\n") - .append(" float x1 = x0 - float(i1) + 0.1666666667f;\n") - .append(" float y1 = y0 - float(j1) + 0.1666666667f;\n") - .append(" float z1 = z0 - float(k1) + 0.1666666667f;\n") - .append(" float x2 = x0 - float(i2) + 0.3333333333f;\n") - .append(" float y2 = y0 - float(j2) + 0.3333333333f;\n") - .append(" float z2 = z0 - float(k2) + 0.3333333333f;\n") - .append(" float x3 = x0 - 0.5f;\n") - .append(" float y3 = y0 - 0.5f;\n") - .append(" float z3 = z0 - 0.5f;\n") - .append(" int ii = i & 255;\n") - .append(" int jj = j & 255;\n") - .append(" int kk = k & 255;\n") - .append(" int g0 = ").append(perms.access("(ii + " + perms.access("(jj + " + perms.access("kk") + ") & 255") + ") & 255")).append(" % 12;\n") - .append(" int g1 = ").append(perms.access("(ii + i1 + " + perms.access("(jj + j1 + " + perms.access("(kk + k1) & 255") + ") & 255") + ") & 255")).append(" % 12;\n") - .append(" int g2 = ").append(perms.access("(ii + i2 + " + perms.access("(jj + j2 + " + perms.access("(kk + k2) & 255") + ") & 255") + ") & 255")).append(" % 12;\n") - .append(" int g3 = ").append(perms.access("(ii + 1 + " + perms.access("(jj + 1 + " + perms.access("(kk + 1) & 255") + ") & 255") + ") & 255")).append(" % 12;\n") - .append(" float d0 = 0.6f - x0*x0 - y0*y0 - z0*z0;\n") - .append(" float n0 = (d0 < 0.0f) ? 0.0f : d0*d0*d0*d0 * (float(GX[g0])*x0 + float(GY[g0])*y0 + float(GZ[g0])*z0);\n") - .append(" float d1 = 0.6f - x1*x1 - y1*y1 - z1*z1;\n") - .append(" float n1 = (d1 < 0.0f) ? 0.0f : d1*d1*d1*d1 * (float(GX[g1])*x1 + float(GY[g1])*y1 + float(GZ[g1])*z1);\n") - .append(" float d2 = 0.6f - x2*x2 - y2*y2 - z2*z2;\n") - .append(" float n2 = (d2 < 0.0f) ? 0.0f : d2*d2*d2*d2 * (float(GX[g2])*x2 + float(GY[g2])*y2 + float(GZ[g2])*z2);\n") - .append(" float d3 = 0.6f - x3*x3 - y3*y3 - z3*z3;\n") - .append(" float n3 = (d3 < 0.0f) ? 0.0f : d3*d3*d3*d3 * (float(GX[g3])*x3 + float(GY[g3])*y3 + float(GZ[g3])*z3);\n") - .append(" return 32.0f * (n0 + n1 + n2 + n3);\n") - .append("}\n"); - - String result = builder.createName("simplex"); - builder.logic.append(" float ").append(result).append(" = ") - .append(funcName).append("(").append(x).append(", ").append(y).append(", ").append(z).append(");\n"); - return result; + 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 index 4597d296..44d46648 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/CCEnhancedWorldGenerator.java +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/CCEnhancedWorldGenerator.java @@ -1,14 +1,12 @@ package com.cardinalstar.cubicchunks.worldgen.ccenhanced; -import static org.lwjgl.opengl.GL20.glUseProgram; -import static org.lwjgl.opengl.GL43.glDispatchCompute; +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.Objects; import java.util.Random; import javax.annotation.Nullable; @@ -25,22 +23,20 @@ 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.KernelSubmission; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelSubmissionResult; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelSubmissionToken; import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.Noise2DKernelExecutor; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferAllocator; +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.BufferDescriptor; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.GPUBuffer; +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; @@ -65,6 +61,7 @@ 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; @@ -119,7 +116,7 @@ public GenerationResult provideColumn(World world, int columnX, int colum ColumnContext ctx = getOrComputeContext(chunk, columnX, columnZ); BiomeLookupResult[] grid = biomeCache.getGrid(columnX, columnZ); -// fillBiomeArray(chunk, grid); + // fillBiomeArray(chunk, grid); ExtendedBlockStorage[] ebsArr = new ExtendedBlockStorage[16]; for (int cubeY = 0; cubeY < 16; cubeY++) { @@ -156,7 +153,7 @@ public GenerationResult provideCube(@Nullable Chunk chunk, int cubeX, int ColumnContext ctx = getOrComputeContext(chunk, cubeX, cubeZ); BiomeLookupResult[] grid = biomeCache.getGrid(cubeX, cubeZ); -// fillBiomeArray(chunk, grid); + // fillBiomeArray(chunk, grid); ExtendedBlockStorage[] ebsArr = new ExtendedBlockStorage[16]; for (int cy = 0; cy < 16; cy++) { @@ -266,31 +263,46 @@ private static long packKey(int chunkX, int chunkZ) { private BlockGenKernelExecutor blockKernel; @Override - public ComputePlan plan( - @Nullable Chunk column, int columnX, int columnZ, - IntArrayList cubeYLevels - ) { + 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(); - KernelContext.getScheduler().runAndWait(() -> { - 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(); - }); + 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(); @@ -298,24 +310,37 @@ public ComputePlan plan( // 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, null, ImmutableMap.of("temperature", temperature, "humidity", humidity, "continentalness", continentalness, "erosion", erosion)); - - var lookup = plan.submit(lookupKernel, null, ImmutableMap.of("distances", distances.get("distances"))); + 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, null, ImmutableMap.of( - "distances", distances.get("distances"), - "hvNoise", hvNoise.get("noise") - )); + var heightData = plan.submit( + heightMapKernel, + ImmutableMap.of("distance", distances.get("distance"), "hvNoise", hvNoise.get("noise"))); IntOpenHashSet cubesToGenerate = new IntOpenHashSet(cubeYLevels); @@ -334,7 +359,10 @@ public ComputePlan plan( } for (Integer cubeY : cubesToGenerate) { - var blockData = plan.submit(blockKernel, new CubePos(columnX, cubeY, columnZ), ImmutableMap.of("heightMap", heightData.get("heightMap"))); + var blockData = plan.submit( + blockKernel, + new CubePos(columnX, cubeY, columnZ), + ImmutableMap.of("height", heightData.get("height"))); Cube cube = new Cube(column, cubeY); @@ -345,17 +373,17 @@ public ComputePlan 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); -// } -// } + // 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); } @@ -373,7 +401,8 @@ private static void putBiome(Chunk column, int x, int z, CCBiomeGenBase biome) { } private void processCube(Map inputs, Cube cube) { - var rawBlockData = inputs.get("blocks").asIntBuffer(); + var rawBlockData = inputs.get("block") + .asIntBuffer(); ExtendedBlockStorage ebs = cube.getOrCreateStorage(); @@ -384,8 +413,8 @@ private void processCube(Map inputs, Cube cube) { int grass = Block.getIdFromBlock(Blocks.grass); int log = Block.getIdFromBlock(Blocks.log); - for (int y = 0; y < 16; y++) { - for (int z = 0; z < 16; z++) { + 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); @@ -404,100 +433,58 @@ private void processCube(Map inputs, Cube cube) { getCubeLoader().addCube(cube); } - private static class BlockGenKernelExecutor implements KernelExecutor { + private static class BlockGenKernelExecutor extends StandardKernelExecutor { - private final int program; + @Override + protected String generateKernel(KernelBuilder builder) { + builder.addParameter(BufferDataType.i32, "cubeY"); - public BlockGenKernelExecutor() { - String code = """ - #version 430 core + builder.addInputBuffer("height", new BufferLayout(BufferDataType.f32, 16, 16)); - layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in; + builder.addOutputBuffer("block", new BufferLayout(BufferDataType.u32, 16, 16, 16)); - $uniform + return """ + #version 460 - layout(std430, binding = 0) buffer Arena { uint arena[]; }; - layout(std430, binding = 1) readonly buffer Uniforms { BlockPickerUniform uniforms[]; }; + layout(local_size_x = 16, local_size_y = 16) in; - void main() { - uint id = gl_WorkGroupID.x; + layout(set = 0, binding = 0) readonly buffer Constants { uint constants[]; }; + layout(set = 1, binding = 0) buffer Arena { uint arena[]; }; - uint lx = gl_LocalInvocationID.x; - uint ly = gl_LocalInvocationID.y; - uint lz = gl_LocalInvocationID.z + gl_WorkGroupID.z; + $preamble - int cubeY = uniforms[id].cubeY; - uint heightmapOffset = uniforms[id].heightmapOffset; - uint blockOffset = uniforms[id].blockOffset; + $pc - int gy = int(ly) + (cubeY << 4); + void main() { + uint x = gl_GlobalInvocationID.x; + uint y = gl_GlobalInvocationID.y; + uint z = gl_GlobalInvocationID.z; - int topBlock = int(uintBitsToFloat(arena[heightmapOffset + ((lz << 4) | lx)])); + uint blockColumn = (z << 4) | x; - uint block = 0; // air + int gy = int(y) + (GET_CUBE_Y << 4); - block = gy < topBlock ? 2 : block; // dirt - block = gy < topBlock - 3 ? 1 : block; // stone overrides dirt for deep blocks - block = gy == topBlock ? 3 : block; // grass + int topBlock = 60; - arena[blockOffset + ((lz << 8) | (ly << 4) | lx)] = block; - } - """ - .replace("$uniform", BlockPickerUniformGLStruct.SOURCE); + uint block = 0; // air - this.program = KernelExecutor.createProgram(code); + 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 - public Map getOutputs( - ComputePlan plan, - KernelSubmissionToken submission, - CubePos cubePos, - Map inputs - ) { - var heightMap = inputs.get("heightMap"); - Objects.requireNonNull(heightMap, "expected heightMap input buffer"); - heightMap.assertLayout(BufferDataType.f32, 16, 16); - - return ImmutableMap.of("blocks", plan.describeBuffer(submission, BufferDataType.u32, 16, 16, 16)); + protected Map getParameters(CubePos cubePos) { + return ImmutableMap.of("cubeY", cubePos.getY()); } @Override - public KernelSubmissionResult[] submit(BufferAllocator alloc, KernelSubmission[] submissions) { - glUseProgram(this.program); - - BlockPickerUniformPrimitiveBuffer uniforms = new BlockPickerUniformPrimitiveBuffer(submissions.length); - - KernelSubmissionResult[] results = new KernelSubmissionResult[submissions.length]; - - uniforms.forEachFast((i, view) -> { - var key = submissions[i].key(); - - GPUBuffer input = submissions[i].inputs().get("heightMap"); - - GPUBuffer output = alloc.alloc(BufferDataType.u32, 16, 16, 16); - results[i] = new KernelSubmissionResult(ImmutableMap.of("blocks", output)); - - view.setCubeY(key.getY()); - view.setHeightmapOffset(input.getBufferOffset() / 4); - view.setBlockOffset(output.getBufferOffset() / 4); - - return true; - }); - - GPUBuffer uniformGPU = alloc.uniform(uniforms); - - alloc.bindSSBO(0); - uniformGPU.bind(1); - - glDispatchCompute(submissions.length, 1, 16); - - alloc.unbindSSBO(0); - uniformGPU.unbind(1); - - glUseProgram(0); - - return results; + protected void dispatch(VkCommandBuffer commands) { + vkCmdDispatch(commands, 1, 1, 16); } } } 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 index 793d2b1c..4493e188 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/BiomeDistanceKernelExecutor.java +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/BiomeDistanceKernelExecutor.java @@ -1,131 +1,72 @@ package com.cardinalstar.cubicchunks.worldgen.ccenhanced.biome; -import static org.lwjgl.opengl.GL20.glUseProgram; -import static org.lwjgl.opengl.GL43.glDispatchCompute; - -import java.util.Map; - -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.ComputePlan; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelConstantBuilder; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelExecutor; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelSubmission; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelSubmissionResult; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelSubmissionToken; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferAllocator; +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.BufferDescriptor; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.GPUBuffer; -import com.google.common.collect.ImmutableMap; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferLayout; -public class BiomeDistanceKernelExecutor implements KernelExecutor { +public class BiomeDistanceKernelExecutor extends StandardKernelExecutor { @org.jetbrains.annotations.NotNull private final CCBiomeGenBase[] biomes; - private final int program; - public BiomeDistanceKernelExecutor(CCBiomeGenBase[] biomes) { this.biomes = biomes; + } - KernelConstantBuilder biomeCoords = new KernelConstantBuilder("float", "BIOME_COORDS"); + @Override + protected String generateKernel(KernelBuilder builder) { + float[] climatePoints = new float[biomes.length * 4]; for (int i = 0; i < biomes.length; i++) { - biomeCoords.annotate(biomes[i].biomeName); - biomeCoords.add(biomes[i].climatePoint); + System.arraycopy(biomes[i].climatePoint, 0, climatePoints, i * 4, 4); } - String code = - """ - #version 430 core + builder.addBufferMacros("BIOME_COORDS", builder.addConstant(climatePoints)); - layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in; + 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)); - $uniform + builder.addOutputBuffer("distance", new BufferLayout(BufferDataType.f32, 16, 16, biomes.length)); - layout(std430, binding = 0) buffer Arena { float arena[]; }; - layout(std430, binding = 1) readonly buffer Uniforms { BiomeDistanceUniform uniforms[]; }; + return """ + #version 460 - $biomeCoords + layout(local_size_x = 16, local_size_y = 16) in; - void main() { - uint id = gl_WorkGroupID.x; + layout(set = 0, binding = 0) readonly buffer Constants { uint constants[]; }; + layout(set = 1, binding = 0) buffer Arena { uint arena[]; }; + + $preamble - uint x = gl_LocalInvocationID.x; - uint y = gl_LocalInvocationID.y; - uint biome = gl_LocalInvocationID.z + gl_WorkGroupID.z; + $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; - uint temperatureOffset = uniforms[id].temperatureOffset; - uint humidityOffset = uniforms[id].humidityOffset; - uint continentalnessOffset = uniforms[id].continentalnessOffset; - uint erosionOffset = uniforms[id].erosionOffset; - uint distanceOffset = uniforms[id].distanceOffset; - - float a = arena[pixelIndex + temperatureOffset] - BIOME_COORDS[biomeIndex + 0u]; - float b = arena[pixelIndex + humidityOffset] - BIOME_COORDS[biomeIndex + 1u]; - float c = arena[pixelIndex + continentalnessOffset] - BIOME_COORDS[biomeIndex + 2u]; - float d = arena[pixelIndex + erosionOffset] - BIOME_COORDS[biomeIndex + 3u]; + 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); - arena[biome * 256u + pixelIndex + distanceOffset] = sqrt(a * a + b * b + c * c + d * d); + SET_DISTANCE(biome * 256u + pixelIndex, sqrt(a * a + b * b + c * c + d * d)); } - """ - .replace("$biomeCount", Integer.toString(biomes.length)) - .replace("$uniform", BiomeDistanceUniformGLStruct.SOURCE) - .replace("$biomeCoords", biomeCoords.toString()); - - this.program = KernelExecutor.createProgram(code); - } - - @Override - public Map getOutputs( - ComputePlan plan, KernelSubmissionToken submission, Void key, - Map inputs - ) { - inputs.get("temperature").assertLayout(BufferDataType.f32, 16, 16); - inputs.get("humidity").assertLayout(BufferDataType.f32, 16, 16); - inputs.get("continentalness").assertLayout(BufferDataType.f32, 16, 16); - inputs.get("erosion").assertLayout(BufferDataType.f32, 16, 16); - - return ImmutableMap.of("distances", plan.describeBuffer(submission, BufferDataType.f32, 16, 16, biomes.length)); + """; } @Override - public KernelSubmissionResult[] submit(BufferAllocator alloc, KernelSubmission[] submissions) { - glUseProgram(this.program); - - BiomeDistanceUniformPrimitiveBuffer uniforms = new BiomeDistanceUniformPrimitiveBuffer(submissions.length); - - KernelSubmissionResult[] results = new KernelSubmissionResult[submissions.length]; - - uniforms.forEachFast((i, view) -> { - GPUBuffer output = alloc.alloc(BufferDataType.f32, 16, 16, biomes.length); - results[i] = new KernelSubmissionResult(ImmutableMap.of("distances", output)); - - Map inputs = submissions[i].inputs(); - - view.setTemperatureOffset(inputs.get("temperature").getBufferOffset() / 4); - view.setHumidityOffset(inputs.get("humidity").getBufferOffset() / 4); - view.setContinentalnessOffset(inputs.get("continentalness").getBufferOffset() / 4); - view.setErosionOffset(inputs.get("erosion").getBufferOffset() / 4); - view.setDistanceOffset(output.getBufferOffset() / 4); - - return true; - }); - - GPUBuffer uniformGPU = alloc.uniform(uniforms); - - alloc.bindSSBO(0); - uniformGPU.bind(1); - - glDispatchCompute(submissions.length, 1, biomes.length); - - alloc.unbindSSBO(0); - uniformGPU.unbind(1); - - glUseProgram(0); - - return results; + 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 index 44f586bc..57914fe5 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/BiomeLookupKernelExecutor.java +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/BiomeLookupKernelExecutor.java @@ -1,60 +1,53 @@ package com.cardinalstar.cubicchunks.worldgen.ccenhanced.biome; -import static org.lwjgl.opengl.GL20.glUseProgram; -import static org.lwjgl.opengl.GL43.glDispatchCompute; - -import java.util.Map; - -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.ComputePlan; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelExecutor; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelSubmission; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelSubmissionResult; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelSubmissionToken; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferAllocator; +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.BufferDescriptor; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.GPUBuffer; -import com.google.common.collect.ImmutableMap; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferLayout; -public class BiomeLookupKernelExecutor implements KernelExecutor { +public class BiomeLookupKernelExecutor extends StandardKernelExecutor { - private final int program; private final int biomeCount; public BiomeLookupKernelExecutor(int biomeCount) { this.biomeCount = biomeCount; + } - String code = - """ - #version 430 core + @Override + protected String generateKernel(KernelBuilder builder) { + builder.addInputBuffer("distance", new BufferLayout(BufferDataType.f32, 16, 16, biomeCount)); - layout(local_size_x = 16, local_size_z = 16) in; + builder.addOutputBuffer("closestBiome", new BufferLayout(BufferDataType.i32, 16, 16, 4)); + builder.addOutputBuffer("biomeWeight", new BufferLayout(BufferDataType.f32, 16, 16, 4)); - $uniform + builder.addMacro("BIOME_COUNT", biomeCount); - layout(std430, binding = 0) buffer Arena { uint arena[]; }; - layout(std430, binding = 1) readonly buffer Uniforms { BiomeLookupUniform uniforms[]; }; + return """ + #version 460 - const uint BIOME_STRIDE = 16 * 16; + layout(local_size_x = 16, local_size_y = 16) in; - void main() { - uint id = gl_WorkGroupID.x; + layout(set = 0, binding = 0) readonly buffer Constants { uint constants[]; }; + layout(set = 1, binding = 0) buffer Arena { uint arena[]; }; + + $preamble + + $pc - uint x = gl_LocalInvocationID.x; - uint z = gl_LocalInvocationID.z; + const uint BIOME_STRIDE = 16u * 16u; - uint distanceOffset = uniforms[id].distanceOffset; - uint closestOffset = uniforms[id].closestOffset; - uint weightsOffset = uniforms[id].weightsOffset; + void main() { + uint x = gl_GlobalInvocationID.x; + uint y = gl_GlobalInvocationID.y; - uint columnOffset = z << 4 | x; + 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 = 0; i < $biomeCount; i++) { - float value = uintBitsToFloat(arena[BIOME_STRIDE * i + columnOffset + distanceOffset]); + 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); @@ -65,10 +58,10 @@ void main() { } // Write closest biome indices - arena[columnOffset * 4u + 0u + closestOffset] = uint(idx[0]); - arena[columnOffset * 4u + 1u + closestOffset] = uint(idx[1]); - arena[columnOffset * 4u + 2u + closestOffset] = uint(idx[2]); - arena[columnOffset * 4u + 3u + closestOffset] = uint(idx[3]); + 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); @@ -79,64 +72,11 @@ void main() { float norm = 1.0f / (invA + invB + invC + invD); // Write biome weights - arena[columnOffset * 4 + 0 + weightsOffset] = floatBitsToUint(invA * norm); - arena[columnOffset * 4 + 1 + weightsOffset] = floatBitsToUint(invB * norm); - arena[columnOffset * 4 + 2 + weightsOffset] = floatBitsToUint(invC * norm); - arena[columnOffset * 4 + 3 + weightsOffset] = floatBitsToUint(invD * norm); + 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); } - """ - .replace("$uniform", BiomeLookupUniformGLStruct.SOURCE) - .replace("$biomeCount", Integer.toString(biomeCount)); - - this.program = KernelExecutor.createProgram(code); - } - - @Override - public Map getOutputs( - ComputePlan plan, KernelSubmissionToken submission, Void key, - Map inputs - ) { - inputs.get("distances").assertLayout(BufferDataType.f32, 16, 16, biomeCount); - - return ImmutableMap.of( - "closestBiomes", plan.describeBuffer(submission, BufferDataType.i32, 16, 16, 4), - "biomeWeights", plan.describeBuffer(submission, BufferDataType.f32, 16, 16, 4)); - } - - @Override - public KernelSubmissionResult[] submit(BufferAllocator alloc, KernelSubmission[] submissions) { - glUseProgram(this.program); - - BiomeLookupUniformPrimitiveBuffer uniforms = new BiomeLookupUniformPrimitiveBuffer(submissions.length); - - KernelSubmissionResult[] results = new KernelSubmissionResult[submissions.length]; - - uniforms.forEachFast((i, view) -> { - GPUBuffer distances = submissions[i].inputs().get("distances"); - GPUBuffer closestBiomes = alloc.alloc(BufferDataType.i32, 16, 16, 4); - GPUBuffer biomeWeights = alloc.alloc(BufferDataType.f32, 16, 16, 4); - - results[i] = new KernelSubmissionResult(ImmutableMap.of("closestBiomes", closestBiomes, "biomeWeights", biomeWeights)); - - view.setDistanceOffset(distances.getBufferOffset() / 4); - view.setClosestOffset(closestBiomes.getBufferOffset() / 4); - view.setWeightsOffset(biomeWeights.getBufferOffset() / 4); - - return true; - }); - - GPUBuffer uniformGPU = alloc.uniform(uniforms); - - alloc.bindSSBO(0); - uniformGPU.bind(1); - - glDispatchCompute(submissions.length, 1, 1); - - alloc.unbindSSBO(0); - uniformGPU.unbind(1); - - glUseProgram(0); - - return results; + """; } } 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 index e3c038a8..40241cda 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/CCBiomeRegistry.java +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/CCBiomeRegistry.java @@ -89,10 +89,8 @@ private static int[] topNSorted(float[] dists, int n) { 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) -> { + 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; 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 index e7150b9e..02be7c7e 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/HeightMapKernelExecutor.java +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/biome/HeightMapKernelExecutor.java @@ -1,22 +1,13 @@ package com.cardinalstar.cubicchunks.worldgen.ccenhanced.biome; -import static org.lwjgl.opengl.GL20.glUseProgram; -import static org.lwjgl.opengl.GL43.glDispatchCompute; - import java.util.List; -import java.util.Map; - -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.ComputePlan; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelConstantBuilder; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelExecutor; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelSubmission; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelSubmissionResult; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelSubmissionToken; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferAllocator; + +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.BufferDescriptor; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.GPUBuffer; -import com.google.common.collect.ImmutableMap; +import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferLayout; /** * Blends per-biome rootHeight and heightVariation using Gaussian falloff over all biomes in @@ -30,135 +21,81 @@ *

* Output: heightMap[col] = blendedRoot + blendedVar * hvNoise[col]. */ -public class HeightMapKernelExecutor implements KernelExecutor { +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}. + * Tune alongside the climate axis frequency scales in + * {@link com.cardinalstar.cubicchunks.worldgen.ccenhanced.climate.ClimateSystem}. */ private static final float FALLOFF = 10.0f; - private final int program; - private final int biomeCount; + @NotNull + private final List biomes; /** Embeds per-biome rootHeight and heightVariation as GLSL constant arrays. */ public HeightMapKernelExecutor(List biomes) { - this.biomeCount = biomes.size(); + this.biomes = biomes; + } - KernelConstantBuilder biomeRoot = new KernelConstantBuilder("float", "BIOME_ROOT"); - KernelConstantBuilder biomeVar = new KernelConstantBuilder("float", "BIOME_VAR"); + @Override + protected String generateKernel(KernelBuilder builder) { + float[] rootHeight = new float[biomes.size()]; + float[] heightVariation = new float[biomes.size()]; - for (int i = 0; i < biomeCount; i++) { + for (int i = 0; i < biomes.size(); i++) { CCBiomeGenBase b = biomes.get(i); - biomeRoot.annotate(b.biomeName); - biomeRoot.add(b.rootHeight); - - biomeVar.annotate(b.biomeName); - biomeVar.add(b.heightVariation); + rootHeight[i] = b.rootHeight; + heightVariation[i] = b.heightVariation; } - String code = """ - #version 430 core + builder.addBufferMacros("ROOT_HEIGHT", builder.addConstant(rootHeight)); + builder.addBufferMacros("HEIGHT_VARIATION", builder.addConstant(heightVariation)); + builder.addMacro("FALLOFF", FALLOFF); + builder.addMacro("BIOME_COUNT", biomes.size()); - layout(local_size_x = 16, local_size_z = 16) in; + builder.addInputBuffer("distance", new BufferLayout(BufferDataType.f32, 16, 16, biomes.size())); + builder.addInputBuffer("hvNoise", new BufferLayout(BufferDataType.f32, 16, 16)); - $uniform + builder.addOutputBuffer("height", new BufferLayout(BufferDataType.f32, 16, 16)); - // distances[biome * 256 + columnOffset] — axis-major layout from BiomeDistanceKernelExecutor - layout(std430, binding = 0) buffer Arena { float arena[]; }; - layout(std430, binding = 1) readonly buffer Uniforms { HeightMapUniform uniforms[]; }; + return """ + #version 460 - $biomeRoot - $biomeVar + layout(local_size_x = 16, local_size_y = 16) in; - const float FALLOFF = $falloff; + layout(set = 0, binding = 0) readonly buffer Constants { uint constants[]; }; + layout(set = 1, binding = 0) buffer Arena { uint arena[]; }; - void main() { - uint id = gl_WorkGroupID.x; + $preamble - uint x = gl_LocalInvocationID.x; - uint z = gl_LocalInvocationID.z; + $pc - uint distancesOffset = uniforms[id].distancesOffset; - uint hvNoiseOffset = uniforms[id].hvNoiseOffset; - uint heightMapOffset = uniforms[id].heightMapOffset; + void main() { + uint x = gl_GlobalInvocationID.x; + uint y = gl_GlobalInvocationID.y; - uint columnOffset = z << 4u | x; + uint columnOffset = y << 4u | x; float blendedRoot = 0.0f; float blendedVar = 0.0f; float totalWeight = 0.0f; - for (int i = 0; i < $biomeCount; i++) { - float dist = arena[uint(i) * 256u + columnOffset + distancesOffset]; + for (uint biome = 0u; biome < uint(BIOME_COUNT); biome++) { + float dist = GET_DISTANCE(biome * 256u + columnOffset); float w = exp(-FALLOFF * dist * dist); - blendedRoot += BIOME_ROOT[i] * w; - blendedVar += BIOME_VAR[i] * w; + blendedRoot += GET_ROOT_HEIGHT(biome) * w; + blendedVar += GET_HEIGHT_VARIATION(biome) * w; totalWeight += w; } blendedRoot /= totalWeight; blendedVar /= totalWeight; - arena[columnOffset + heightMapOffset] = blendedRoot + blendedVar * arena[columnOffset + hvNoiseOffset]; + SET_HEIGHT(columnOffset, blendedRoot + blendedVar * GET_HV_NOISE(columnOffset)); } - """ - .replace("$uniform", HeightMapUniformGLStruct.SOURCE) - .replace("$biomeRoot", biomeRoot.toString()) - .replace("$biomeVar", biomeVar.toString()) - .replace("$biomeCount", Integer.toString(biomeCount)) - .replace("$falloff", FALLOFF + "f"); - - this.program = KernelExecutor.createProgram(code); - } - - @Override - public Map getOutputs( - ComputePlan plan, KernelSubmissionToken submission, Void key, - Map inputs - ) { - inputs.get("distances").assertLayout(BufferDataType.f32, 16, 16, biomeCount); - inputs.get("hvNoise").assertLayout(BufferDataType.f32, 16, 16); - - return ImmutableMap.of("heightMap", plan.describeBuffer(submission, BufferDataType.f32, 16, 16)); - } - - @Override - public KernelSubmissionResult[] submit(BufferAllocator alloc, KernelSubmission[] submissions) { - glUseProgram(this.program); - - HeightMapUniformPrimitiveBuffer uniforms = new HeightMapUniformPrimitiveBuffer(submissions.length); - - KernelSubmissionResult[] results = new KernelSubmissionResult[submissions.length]; - - uniforms.forEachFast((i, view) -> { - GPUBuffer distances = submissions[i].inputs().get("distances"); - GPUBuffer hvNoise = submissions[i].inputs().get("hvNoise"); - GPUBuffer heightMap = alloc.alloc(BufferDataType.i32, 16, 16); - - results[i] = new KernelSubmissionResult(ImmutableMap.of("heightMap", heightMap)); - - view.setDistancesOffset(distances.getBufferOffset() / 4); - view.setHvNoiseOffset(hvNoise.getBufferOffset() / 4); - view.setHeightMapOffset(heightMap.getBufferOffset() / 4); - - return true; - }); - - GPUBuffer uniformGPU = alloc.uniform(uniforms); - - alloc.bindSSBO(0); - uniformGPU.bind(1); - - glDispatchCompute(submissions.length, 1, 1); - - alloc.unbindSSBO(0); - uniformGPU.unbind(1); - - glUseProgram(0); - - return results; + """; } } 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 index 6ef6e96f..8c0da865 100644 --- 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 @@ -6,6 +6,7 @@ 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; @@ -110,7 +111,8 @@ public int getBiomeGrassColor(int p_150558_1_, int p_150558_2_, int p_150558_3_) 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(); + 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/ClimateSystem.java b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/climate/ClimateSystem.java index 77f8890a..376b6727 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/climate/ClimateSystem.java +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/climate/ClimateSystem.java @@ -1,8 +1,5 @@ package com.cardinalstar.cubicchunks.worldgen.ccenhanced.climate; -import static org.lwjgl.opengl.GL20.glUseProgram; -import static org.lwjgl.opengl.GL43.glDispatchCompute; - import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -11,29 +8,21 @@ import org.jetbrains.annotations.Nullable; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.ComputePlan; import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelBuilder; import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelExecutor; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelSubmission; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelSubmissionResult; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.KernelSubmissionToken; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.Noise2DUniformGLStruct; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.Noise2DUniformPrimitiveBuffer; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.BufferAllocator; +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.BufferDescriptor; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.ConstantHardwareBuffer; -import com.cardinalstar.cubicchunks.api.worldgen.hwaccel.buffer.GPUBuffer; +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. + * 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. + * 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 { @@ -75,8 +64,8 @@ private void addAxis(long worldSeed, int axisIndex, String name, int octaves, do } /** - * Sample the climate at world coordinates (x, z). - * Domain warping is applied to each axis independently using per-axis coordinate offsets. + * 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()]; @@ -87,131 +76,78 @@ public float[] sample(double x, double z, float @Nullable [] pooled) { // 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); + 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. + * 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 implements KernelExecutor { + private class AxisSamplerKernelExecutor extends StandardKernelExecutor { private final int axisIndex; - private final int program; - - private final ConstantHardwareBuffer constants; + private final ClimateAxis axis; public AxisSamplerKernelExecutor(int axisIndex, ClimateAxis axis) { this.axisIndex = axisIndex; + this.axis = axis; + } - KernelBuilder builder = new KernelBuilder(); + @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()); - float warpOffsetX = (float) WARP_OFFSET_X[axisIndex]; - float warpOffsetZ = (float) WARP_OFFSET_Z[axisIndex]; - int axisBase = axisIndex * 256; + 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); - String code = - """ - #version 430 core + return """ + #version 460 - layout(local_size_x = 16, local_size_z = 16) in; + layout(local_size_x = 16, local_size_y = 16) in; - $uniform + layout(set = 0, binding = 0) readonly buffer Constants { uint constants[]; }; + layout(set = 1, binding = 0) buffer Arena { uint arena[]; }; - layout(std430, binding = 0) writeonly buffer Arena { float arena[]; }; - layout(std430, binding = 1) readonly buffer Uniforms { Noise2DUniform uniforms[]; }; - layout(std430, binding = 2) readonly buffer Constants { uint constants[]; }; + $pc $preamble void main() { - uint id = gl_WorkGroupID.x; - - uint x = gl_LocalInvocationID.x; - uint z = gl_LocalInvocationID.z; - - int offsetX = uniforms[id].offsetX; - int offsetZ = uniforms[id].offsetZ; - uint outputOffset = uniforms[id].outputOffset; + uint x = gl_GlobalInvocationID.x; + uint z = gl_GlobalInvocationID.y; - int gx = offsetX + int(x); - int gz = offsetZ + int(z); + int gx = GET_OFFSET_X + int(x); + int gz = GET_OFFSET_Z + int(z); $logic - float warpX = warp(gx + $warpOffsetX, gz + $warpOffsetZ) * 200.0f; - float warpZ = warp(gx + $warpOffsetX + 500.0f, gz + $warpOffsetZ + 500.0f) * 200.0f; + 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; - arena[(z << 4) + x + outputOffset] = $axisFunc(gx + warpX, gz + warpZ); + SET_NOISE(AXIS_OUTPUT_OFFSET + ((z << 4) | x), $axisFunc(gx + warpX, gz + warpZ)); } - """ - .replace("$uniform", Noise2DUniformGLStruct.SOURCE) - .replace("$preamble", builder.preamble.toString()) - .replace("$logic", builder.logic.toString()) - .replace("$warpOffsetX", warpOffsetX + "f") - .replace("$warpOffsetZ", warpOffsetZ + "f") - .replace("$axisBase", Integer.toString(axisBase)) - .replace("$axisFunc", axis.getName()); - - this.program = KernelExecutor.createProgram(code); - this.constants = builder.constants.finish(); + """.replace("$logic", builder.logic.toString()) + .replace("$axisFunc", axis.getName()); } @Override - public Map getOutputs( - ComputePlan plan, KernelSubmissionToken submission, ChunkCoordIntPair key, - Map inputs - ) { - return ImmutableMap.of("noise", plan.describeBuffer(submission, BufferDataType.f32, 16, 16)); - } - - @Override - public KernelSubmissionResult[] submit(BufferAllocator alloc, KernelSubmission[] submissions) { - glUseProgram(this.program); - - Noise2DUniformPrimitiveBuffer uniforms = new Noise2DUniformPrimitiveBuffer(submissions.length); - - KernelSubmissionResult[] results = new KernelSubmissionResult[submissions.length]; - - uniforms.forEachFast((i, view) -> { - var key = submissions[i].key(); - - GPUBuffer output = alloc.alloc(BufferDataType.f32, 16, 16); - results[i] = new KernelSubmissionResult(ImmutableMap.of("noise", output)); - - view.setOffsetX(key.chunkXPos << 4); - view.setOffsetZ(key.chunkZPos << 4); - view.setOutputOffset(output.getBufferOffset() / 4); - - return true; - }); - - GPUBuffer uniformGPU = alloc.uniform(uniforms); - - alloc.bindSSBO(0); - uniformGPU.bind(1); - constants.bindSSBO(2); - - glDispatchCompute(submissions.length, 1, 1); - - alloc.unbindSSBO(0); - uniformGPU.unbind(1); - constants.unbindSSBO(2); - - glUseProgram(0); - - return results; + 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 index b3c5080f..4d40fc30 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/climate/NoiseClimateAxis.java +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/climate/NoiseClimateAxis.java @@ -22,10 +22,10 @@ public class NoiseClimateAxis implements ClimateAxis { 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) + * @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; @@ -49,12 +49,11 @@ public void compileShader(KernelBuilder builder, String functionName) { String result = this.noise.compileKernel2D(builder, "x", "y"); String function = """ - float $name(float x, float y) { - $logic - return $result; - } - """ - .replace("$name", functionName) + float $name(float x, float y) { + $logic + return $result; + } + """.replace("$name", functionName) .replace("$logic", builder.logic.toString()) .replace("$result", result); 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 index a2b18598..7f40d41a 100644 --- a/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/terrain/CCBiomeCache.java +++ b/src/main/java/com/cardinalstar/cubicchunks/worldgen/ccenhanced/terrain/CCBiomeCache.java @@ -4,6 +4,7 @@ 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; /** @@ -27,7 +28,9 @@ public class CCBiomeCache { 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); + private final Long2ObjectLinkedOpenHashMap cache = new Long2ObjectLinkedOpenHashMap<>( + CACHE_CAPACITY, + 0.75f); public CCBiomeCache(ClimateSystem climate) { this.climate = climate;