diff --git a/src/main/java/me/cortex/zenith/client/IGetVoxelCore.java b/src/main/java/me/cortex/zenith/client/IGetVoxelCore.java index d4bb85e7..8b1552f1 100644 --- a/src/main/java/me/cortex/zenith/client/IGetVoxelCore.java +++ b/src/main/java/me/cortex/zenith/client/IGetVoxelCore.java @@ -4,4 +4,6 @@ import me.cortex.zenith.client.core.VoxelCore; public interface IGetVoxelCore { VoxelCore getVoxelCore(); + + void reloadVoxelCore(); } diff --git a/src/main/java/me/cortex/zenith/client/config/ZenithConfig.java b/src/main/java/me/cortex/zenith/client/config/ZenithConfig.java index d96ac4a9..69337a83 100644 --- a/src/main/java/me/cortex/zenith/client/config/ZenithConfig.java +++ b/src/main/java/me/cortex/zenith/client/config/ZenithConfig.java @@ -1,15 +1,67 @@ package me.cortex.zenith.client.config; -public class ZenithConfig { - int qualityScale; - int ingestThreads; - int savingThreads; - int renderThreads; - int savingCompressionLevel; - StorageConfig storageConfig; +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import net.fabricmc.loader.api.FabricLoader; +import java.io.FileReader; +import java.io.IOException; +import java.lang.reflect.Modifier; +import java.nio.file.Files; +import java.nio.file.Path; + +public class ZenithConfig { + private static final Gson GSON = new GsonBuilder() + .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) + .setPrettyPrinting() + .excludeFieldsWithModifiers(Modifier.PRIVATE) + .create(); + public static ZenithConfig CONFIG = loadOrCreate(); + + public boolean enabled = true; + public int qualityScale = 20; + public int ingestThreads = 2; + public int savingThreads = 10; + public int renderThreads = 5; + public int savingCompressionLevel = 7; + public String storagePath = "zenith_db"; + + transient StorageConfig storageConfig; public static abstract class StorageConfig { } public static class FragmentedStorageConfig extends StorageConfig { } public static class LmdbStorageConfig extends StorageConfig { } + + + + + public static ZenithConfig loadOrCreate() { + var path = getConfigPath(); + if (Files.exists(path)) { + try (FileReader reader = new FileReader(path.toFile())) { + return GSON.fromJson(reader, ZenithConfig.class); + } catch (IOException e) { + System.err.println("Could not parse config"); + e.printStackTrace(); + } + } + return new ZenithConfig(); + } + public void save() { + //Unsafe, todo: fixme! needs to be atomic! + try { + Files.writeString(getConfigPath(), GSON.toJson(this)); + } catch (IOException e) { + System.err.println("Failed to write config file"); + e.printStackTrace(); + } + } + + private static Path getConfigPath() { + return FabricLoader.getInstance() + .getConfigDir() + .resolve("zenith-config.json"); + } + } diff --git a/src/main/java/me/cortex/zenith/client/config/ZenithConfigScreenFactory.java b/src/main/java/me/cortex/zenith/client/config/ZenithConfigScreenFactory.java index 9bf624a7..95574335 100644 --- a/src/main/java/me/cortex/zenith/client/config/ZenithConfigScreenFactory.java +++ b/src/main/java/me/cortex/zenith/client/config/ZenithConfigScreenFactory.java @@ -2,11 +2,105 @@ package me.cortex.zenith.client.config; import com.terraformersmc.modmenu.api.ConfigScreenFactory; import com.terraformersmc.modmenu.api.ModMenuApi; +import me.cortex.zenith.client.IGetVoxelCore; +import me.shedaniel.clothconfig2.api.ConfigBuilder; +import me.shedaniel.clothconfig2.api.ConfigCategory; +import me.shedaniel.clothconfig2.api.ConfigEntryBuilder; +import me.shedaniel.clothconfig2.api.Requirement; +import me.shedaniel.clothconfig2.gui.entries.BooleanListEntry; +import me.shedaniel.clothconfig2.gui.entries.IntegerListEntry; +import me.shedaniel.clothconfig2.gui.entries.IntegerSliderEntry; +import me.shedaniel.clothconfig2.impl.builders.DropdownMenuBuilder; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.text.Text; public class ZenithConfigScreenFactory implements ModMenuApi { - + private static final ZenithConfig DEFAULT = new ZenithConfig(); @Override public ConfigScreenFactory getModConfigScreenFactory() { - return parent -> null; + return parent -> buildConfigScreen(parent, ZenithConfig.CONFIG); } + + private static Screen buildConfigScreen(Screen parent, ZenithConfig config) { + ConfigBuilder builder = ConfigBuilder.create() + .setParentScreen(parent) + .setTitle(Text.translatable("title.zenith.config")); + + + addGeneralCategory(builder, config); + addThreadsCategory(builder, config); + addStorageCategory(builder, config); + + builder.setSavingRunnable(() -> { + //After saving the core should be reloaded/reset + var world = (IGetVoxelCore)MinecraftClient.getInstance().worldRenderer; + if (world != null) { + world.reloadVoxelCore(); + } + ZenithConfig.CONFIG.save(); + }); + + return builder.build(); + } + + private static void addGeneralCategory(ConfigBuilder builder, ZenithConfig config) { + ConfigCategory category = builder.getOrCreateCategory(Text.translatable("zenith.config.general")); + ConfigEntryBuilder entryBuilder = builder.entryBuilder(); + + + category.addEntry(entryBuilder.startBooleanToggle(Text.translatable("zenith.config.general.enabled"), config.enabled) + .setTooltip(Text.translatable("zenith.config.general.enabled.tooltip")) + .setSaveConsumer(val -> config.enabled = val) + .setDefaultValue(DEFAULT.enabled) + .build()); + + category.addEntry(entryBuilder.startIntSlider(Text.translatable("zenith.config.general.quality"), config.qualityScale, 16, 64) + .setTooltip(Text.translatable("zenith.config.general.quality.tooltip")) + .setSaveConsumer(val -> config.qualityScale = val) + .setDefaultValue(DEFAULT.qualityScale) + .build()); + + category.addEntry(entryBuilder.startIntSlider(Text.translatable("zenith.config.general.compression"), config.savingCompressionLevel, 1, 21) + .setTooltip(Text.translatable("zenith.config.general.compression.tooltip")) + .setSaveConsumer(val -> config.savingCompressionLevel = val) + .setDefaultValue(DEFAULT.savingCompressionLevel) + .build()); + } + + private static void addThreadsCategory(ConfigBuilder builder, ZenithConfig config) { + ConfigCategory category = builder.getOrCreateCategory(Text.translatable("zenith.config.threads")); + ConfigEntryBuilder entryBuilder = builder.entryBuilder(); + + category.addEntry(entryBuilder.startIntSlider(Text.translatable("zenith.config.threads.ingest"), config.ingestThreads, 1, Runtime.getRuntime().availableProcessors()) + .setTooltip(Text.translatable("zenith.config.general.ingest.tooltip")) + .setSaveConsumer(val -> config.ingestThreads = val) + .setDefaultValue(DEFAULT.ingestThreads) + .build()); + + category.addEntry(entryBuilder.startIntSlider(Text.translatable("zenith.config.threads.saving"), config.savingThreads, 1, Runtime.getRuntime().availableProcessors()) + .setTooltip(Text.translatable("zenith.config.general.saving.tooltip")) + .setSaveConsumer(val -> config.savingThreads = val) + .setDefaultValue(DEFAULT.savingThreads) + .build()); + + category.addEntry(entryBuilder.startIntSlider(Text.translatable("zenith.config.threads.render"), config.renderThreads, 1, Runtime.getRuntime().availableProcessors()) + .setTooltip(Text.translatable("zenith.config.general.render.tooltip")) + .setSaveConsumer(val -> config.renderThreads = val) + .setDefaultValue(DEFAULT.renderThreads) + .build()); + } + + private static void addStorageCategory(ConfigBuilder builder, ZenithConfig config) { + ConfigCategory category = builder.getOrCreateCategory(Text.translatable("zenith.config.storage")); + ConfigEntryBuilder entryBuilder = builder.entryBuilder(); + + //Temporary until i figure out how to do more complex multi layer configuration for storage + category.addEntry(entryBuilder.startStrField(Text.translatable("zenith.config.storage.path"), config.storagePath) + .setTooltip(Text.translatable("zenith.config.storage.path.tooltip")) + .setSaveConsumer(val -> config.storagePath = val) + .setDefaultValue(DEFAULT.storagePath) + .build()); + } + } diff --git a/src/main/java/me/cortex/zenith/client/core/VoxelCore.java b/src/main/java/me/cortex/zenith/client/core/VoxelCore.java index 4a6795f0..f8870dc1 100644 --- a/src/main/java/me/cortex/zenith/client/core/VoxelCore.java +++ b/src/main/java/me/cortex/zenith/client/core/VoxelCore.java @@ -1,5 +1,6 @@ package me.cortex.zenith.client.core; +import me.cortex.zenith.client.config.ZenithConfig; import me.cortex.zenith.client.core.rendering.*; import me.cortex.zenith.client.core.rendering.building.RenderGenerationService; import me.cortex.zenith.client.core.util.DebugUtil; @@ -39,7 +40,7 @@ import java.util.*; public class VoxelCore { private static final Set biomeTintableAllFaces = new HashSet<>(List.of(Blocks.OAK_LEAVES, Blocks.JUNGLE_LEAVES, Blocks.ACACIA_LEAVES, Blocks.DARK_OAK_LEAVES, Blocks.VINE, Blocks.MANGROVE_LEAVES, Blocks.TALL_GRASS, Blocks.LARGE_FERN, - //Blocks.SHORT_GRASS, + Blocks.SHORT_GRASS, Blocks.SPRUCE_LEAVES, Blocks.BIRCH_LEAVES, @@ -66,31 +67,23 @@ public class VoxelCore { SharedIndexBuffer.INSTANCE.id(); this.renderer = new Gl46FarWorldRenderer(); System.out.println("Renderer initialized"); - this.world = new WorldEngine(new FragmentedStorageBackendAdaptor(), 4, 20, 5);//"storagefile.db"//"ethoslab.db" + this.world = new WorldEngine(new FragmentedStorageBackendAdaptor(new File(ZenithConfig.CONFIG.storagePath)), ZenithConfig.CONFIG.ingestThreads, ZenithConfig.CONFIG.savingThreads, ZenithConfig.CONFIG.savingCompressionLevel, 5);//"storagefile.db"//"ethoslab.db" System.out.println("World engine"); this.renderTracker = new RenderTracker(this.world, this.renderer); - this.renderGen = new RenderGenerationService(this.world,7, this.renderTracker::processBuildResult); + this.renderGen = new RenderGenerationService(this.world,ZenithConfig.CONFIG.renderThreads, this.renderTracker::processBuildResult); this.world.setDirtyCallback(this.renderTracker::sectionUpdated); this.renderTracker.setRenderGen(this.renderGen); System.out.println("Render tracker and generator initialized"); //To get to chunk scale multiply the scale by 2, the scale is after how many chunks does the lods halve - this.distanceTracker = new DistanceTracker(this.renderTracker, 5, 64);//16//64 + this.distanceTracker = new DistanceTracker(this.renderTracker, 5, ZenithConfig.CONFIG.qualityScale); System.out.println("Distance tracker initialized"); this.postProcessing = null;//new PostProcessing(); this.world.getMapper().setCallbacks(this::stateUpdate, this::biomeUpdate); - //Runtime.getRuntime().addShutdownHook(this.shutdownThread); - - - //WorldImporter importer = new WorldImporter(this.world, MinecraftClient.getInstance().world); - //importer.importWorldAsyncStart(new File("saves/New World/region")); - - - for (var state : this.world.getMapper().getStateEntries()) { this.stateUpdate(state); } @@ -100,7 +93,6 @@ public class VoxelCore { } System.out.println("Entry updates applied"); - System.out.println("Voxel core initialized"); } diff --git a/src/main/java/me/cortex/zenith/client/mixin/minecraft/MixinWorldRenderer.java b/src/main/java/me/cortex/zenith/client/mixin/minecraft/MixinWorldRenderer.java index f495b643..d0a124a5 100644 --- a/src/main/java/me/cortex/zenith/client/mixin/minecraft/MixinWorldRenderer.java +++ b/src/main/java/me/cortex/zenith/client/mixin/minecraft/MixinWorldRenderer.java @@ -1,6 +1,7 @@ package me.cortex.zenith.client.mixin.minecraft; import me.cortex.zenith.client.IGetVoxelCore; +import me.cortex.zenith.client.config.ZenithConfig; import me.cortex.zenith.client.core.VoxelCore; import net.minecraft.client.render.*; import net.minecraft.client.util.math.MatrixStack; @@ -28,13 +29,17 @@ public abstract class MixinWorldRenderer implements IGetVoxelCore { @Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/WorldRenderer;setupTerrain(Lnet/minecraft/client/render/Camera;Lnet/minecraft/client/render/Frustum;ZZ)V", shift = At.Shift.AFTER)) private void injectSetup(MatrixStack matrices, float tickDelta, long limitTime, boolean renderBlockOutline, Camera camera, GameRenderer gameRenderer, LightmapTextureManager lightmapTextureManager, Matrix4f projectionMatrix, CallbackInfo ci) { - this.core.renderSetup(this.frustum, camera); + if (this.core != null) { + this.core.renderSetup(this.frustum, camera); + } } @Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/WorldRenderer;renderLayer(Lnet/minecraft/client/render/RenderLayer;Lnet/minecraft/client/util/math/MatrixStack;DDDLorg/joml/Matrix4f;)V", ordinal = 2, shift = At.Shift.AFTER)) private void injectOpaqueRender(MatrixStack matrices, float tickDelta, long limitTime, boolean renderBlockOutline, Camera camera, GameRenderer gameRenderer, LightmapTextureManager lightmapTextureManager, Matrix4f projectionMatrix, CallbackInfo ci) { - var cam = camera.getPos(); - this.core.renderOpaque(matrices, cam.x, cam.y, cam.z); + if (this.core != null) { + var cam = camera.getPos(); + this.core.renderOpaque(matrices, cam.x, cam.y, cam.z); + } } public VoxelCore getVoxelCore() { @@ -58,10 +63,25 @@ public abstract class MixinWorldRenderer implements IGetVoxelCore { } return; } + if (this.core != null) { - throw new IllegalStateException("Core is not null"); + this.core.shutdown(); + this.core = null; + } + if (ZenithConfig.CONFIG.enabled) { + this.core = new VoxelCore(); + } + } + + @Override + public void reloadVoxelCore() { + if (this.core != null) { + this.core.shutdown(); + this.core = null; + } + if (this.world != null && ZenithConfig.CONFIG.enabled) { + this.core = new VoxelCore(); } - this.core = new VoxelCore(); } @Inject(method = "close", at = @At("HEAD")) @@ -72,8 +92,6 @@ public abstract class MixinWorldRenderer implements IGetVoxelCore { } } - - @Redirect(method = "render", at = @At(value = "INVOKE", target = "Ljava/lang/Math;max(FF)F"), require = 0) private float redirectMax(float a, float b) { return a; diff --git a/src/main/java/me/cortex/zenith/common/world/SaveLoadSystem.java b/src/main/java/me/cortex/zenith/common/world/SaveLoadSystem.java index 3b4d8859..06f7c184 100644 --- a/src/main/java/me/cortex/zenith/common/world/SaveLoadSystem.java +++ b/src/main/java/me/cortex/zenith/common/world/SaveLoadSystem.java @@ -9,7 +9,7 @@ import java.nio.ByteBuffer; import static org.lwjgl.util.zstd.Zstd.*; public class SaveLoadSystem { - public static ByteBuffer serialize(WorldSection section) { + public static ByteBuffer serialize(WorldSection section, int compressionLevel) { var data = section.copyData(); var compressed = new Short[data.length]; Long2ShortOpenHashMap LUT = new Long2ShortOpenHashMap(); @@ -48,7 +48,7 @@ public class SaveLoadSystem { raw.limit(raw.position()); raw.rewind(); ByteBuffer compressedData = MemoryUtil.memAlloc((int)ZSTD_COMPRESSBOUND(raw.remaining())); - long compressedSize = ZSTD_compress(compressedData, raw, 7); + long compressedSize = ZSTD_compress(compressedData, raw, compressionLevel); compressedData.limit((int) compressedSize); compressedData.rewind(); MemoryUtil.memFree(raw); diff --git a/src/main/java/me/cortex/zenith/common/world/WorldEngine.java b/src/main/java/me/cortex/zenith/common/world/WorldEngine.java index 6619309f..34184fb8 100644 --- a/src/main/java/me/cortex/zenith/common/world/WorldEngine.java +++ b/src/main/java/me/cortex/zenith/common/world/WorldEngine.java @@ -24,18 +24,18 @@ public class WorldEngine { public void setDirtyCallback(Consumer tracker) { - this.dirtyCallback = dirtyCallback; + this.dirtyCallback = tracker; } public Mapper getMapper() {return this.mapper;} - public WorldEngine(StorageBackend storageBackend, int ingestWorkers, int savingServiceWorkers, int maxMipLayers) { + public WorldEngine(StorageBackend storageBackend, int ingestWorkers, int savingServiceWorkers, int compressionLevel, int maxMipLayers) { this.maxMipLevels = maxMipLayers; this.storage = storageBackend; this.mapper = new Mapper(this.storage); this.sectionTracker = new ActiveSectionTracker(maxMipLayers, this::unsafeLoadSection); - this.savingService = new SectionSavingService(this, savingServiceWorkers); + this.savingService = new SectionSavingService(this, savingServiceWorkers, compressionLevel); this.ingestService = new VoxelIngestService(this, ingestWorkers); } diff --git a/src/main/java/me/cortex/zenith/common/world/service/SectionSavingService.java b/src/main/java/me/cortex/zenith/common/world/service/SectionSavingService.java index 1a6980e9..35dd5379 100644 --- a/src/main/java/me/cortex/zenith/common/world/service/SectionSavingService.java +++ b/src/main/java/me/cortex/zenith/common/world/service/SectionSavingService.java @@ -15,12 +15,13 @@ public class SectionSavingService { private volatile boolean running = true; private final Thread[] workers; + private final int compressionLevel; private final ConcurrentLinkedDeque saveQueue = new ConcurrentLinkedDeque<>(); private final Semaphore saveCounter = new Semaphore(0); private final WorldEngine world; - public SectionSavingService(WorldEngine worldEngine, int workers) { + public SectionSavingService(WorldEngine worldEngine, int workers, int compressionLevel) { this.workers = new Thread[workers]; for (int i = 0; i < workers; i++) { var worker = new Thread(this::saveWorker); @@ -29,7 +30,7 @@ public class SectionSavingService { worker.start(); this.workers[i] = worker; } - + this.compressionLevel = compressionLevel; this.world = worldEngine; } @@ -41,7 +42,7 @@ public class SectionSavingService { section.assertNotFree(); section.inSaveQueue.set(false); - var saveData = SaveLoadSystem.serialize(section); + var saveData = SaveLoadSystem.serialize(section, this.compressionLevel); this.world.storage.setSectionData(section.getKey(), saveData); MemoryUtil.memFree(saveData); diff --git a/src/main/java/me/cortex/zenith/common/world/storage/FragmentedStorageBackendAdaptor.java b/src/main/java/me/cortex/zenith/common/world/storage/FragmentedStorageBackendAdaptor.java index bda6a8b1..ffbad877 100644 --- a/src/main/java/me/cortex/zenith/common/world/storage/FragmentedStorageBackendAdaptor.java +++ b/src/main/java/me/cortex/zenith/common/world/storage/FragmentedStorageBackendAdaptor.java @@ -5,15 +5,22 @@ import me.cortex.zenith.common.world.storage.lmdb.LMDBStorageBackend; import net.minecraft.util.math.random.RandomSeed; import java.io.File; +import java.io.IOException; import java.nio.ByteBuffer; +import java.nio.file.Files; //Segments the section data into multiple dbs public class FragmentedStorageBackendAdaptor extends StorageBackend { private final StorageBackend[] backends = new StorageBackend[32]; - public FragmentedStorageBackendAdaptor() { + public FragmentedStorageBackendAdaptor(File directory) { + try { + Files.createDirectories(directory.toPath()); + } catch (IOException e) { + throw new RuntimeException(e); + } for (int i = 0; i < this.backends.length; i++) { - this.backends[i] = new LMDBStorageBackend(new File("storage-db-"+i+".db")); + this.backends[i] = new LMDBStorageBackend(directory.toPath().resolve("storage-db-"+i+".db").toFile());// } }