World seperation, path config, faster importing/ingesting, fixed render error when client model id hadnt been computed, version bump

This commit is contained in:
mcrcortex
2024-02-12 22:14:54 +10:00
parent 99d821e1ec
commit 3096745982
15 changed files with 232 additions and 131 deletions

View File

@@ -8,7 +8,7 @@ yarn_mappings=1.20.4+build.1
loader_version=0.15.0
# Mod Properties
mod_version = 0.0.6-alpha
mod_version = 0.0.7-alpha
maven_group = me.cortex
archives_base_name = voxy

View File

@@ -26,8 +26,6 @@ public class VoxyConfig {
public int ingestThreads = 5;
public int savingThreads = 10;
public int renderThreads = 5;
public int savingCompressionLevel = 7;
public String storagePath = "voxy_db";
public static VoxyConfig loadOrCreate() {

View File

@@ -39,7 +39,7 @@ public class VoxyConfigScreenFactory implements ModMenuApi {
VoxyConfig.CONFIG.save();
});
return ClothConfigDemo.getConfigBuilderWithDemo().build();
return builder.build();//ClothConfigDemo.getConfigBuilderWithDemo().build();
}
private static void addGeneralCategory(ConfigBuilder builder, VoxyConfig config) {
@@ -84,11 +84,11 @@ public class VoxyConfigScreenFactory implements ModMenuApi {
.setDefaultValue(DEFAULT.maxSections)
.build());
category.addEntry(entryBuilder.startIntSlider(Text.translatable("voxy.config.general.compression"), config.savingCompressionLevel, 1, 21)
.setTooltip(Text.translatable("voxy.config.general.compression.tooltip"))
.setSaveConsumer(val -> config.savingCompressionLevel = val)
.setDefaultValue(DEFAULT.savingCompressionLevel)
.build());
//category.addEntry(entryBuilder.startIntSlider(Text.translatable("voxy.config.general.compression"), config.savingCompressionLevel, 1, 21)
// .setTooltip(Text.translatable("voxy.config.general.compression.tooltip"))
// .setSaveConsumer(val -> config.savingCompressionLevel = val)
// .setDefaultValue(DEFAULT.savingCompressionLevel)
// .build());
}
private static void addThreadsCategory(ConfigBuilder builder, VoxyConfig config) {
@@ -118,12 +118,12 @@ public class VoxyConfigScreenFactory implements ModMenuApi {
ConfigCategory category = builder.getOrCreateCategory(Text.translatable("voxy.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("voxy.config.storage.path"), config.storagePath)
.setTooltip(Text.translatable("voxy.config.storage.path.tooltip"))
.setSaveConsumer(val -> config.storagePath = val)
.setDefaultValue(DEFAULT.storagePath)
.build());
////Temporary until i figure out how to do more complex multi layer configuration for storage
//category.addEntry(entryBuilder.startStrField(Text.translatable("voxy.config.storage.path"), config.storagePath)
// .setTooltip(Text.translatable("voxy.config.storage.path.tooltip"))
// .setSaveConsumer(val -> config.storagePath = val)
// .setDefaultValue(DEFAULT.storagePath)
// .build());
}
}

View File

@@ -0,0 +1,7 @@
package me.cortex.voxy.client.core.model;
public class IdNotYetComputedException extends RuntimeException {
public IdNotYetComputedException(int id) {
super("Id not yet computed: " + id);
}
}

View File

@@ -523,7 +523,7 @@ public class ModelManager {
map = this.idMappings[blockId];
}
if (map == -1) {
throw new IllegalArgumentException("Id hasnt been computed yet: " + blockId);
throw new IdNotYetComputedException(blockId);
}
return this.metadataCache[map];
//int map = 0;
@@ -541,7 +541,7 @@ public class ModelManager {
public int getModelId(int blockId) {
int map = this.idMappings[blockId];
if (map == -1) {
throw new IllegalArgumentException("Id hasnt been computed yet: " + blockId);
throw new IdNotYetComputedException(blockId);
}
return map;
}

View File

@@ -1,6 +1,7 @@
package me.cortex.voxy.client.core.rendering.building;
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
import me.cortex.voxy.client.core.model.IdNotYetComputedException;
import me.cortex.voxy.client.core.model.ModelManager;
import me.cortex.voxy.common.world.WorldEngine;
import me.cortex.voxy.common.world.WorldSection;
@@ -56,15 +57,25 @@ public class RenderGenerationService {
continue;
}
section.assertNotFree();
var mesh = factory.generateMesh(section);
BuiltSection mesh = null;
try {
mesh = factory.generateMesh(section);
} catch (IdNotYetComputedException e) {
//We need to reinsert the build task into the queue
System.err.println("Render task failed to complete due to un-computed client id");
synchronized (this.taskQueue) {
this.taskQueue.computeIfAbsent(section.key, key->{this.taskCounter.release(); return task;});
}
}
section.release();
if (mesh != null) {
this.resultConsumer.accept(mesh.clone());
if (!this.meshCache.putMesh(mesh)) {
mesh.free();
}
}
}
}
//TODO: Add a priority system, higher detail sections must always be updated before lower detail
// e.g. priorities NONE->lvl0 and lvl1 -> lvl0 over lvl0 -> lvl1

View File

@@ -5,12 +5,15 @@ import me.cortex.voxy.client.core.util.ByteBufferBackedInputStream;
import me.cortex.voxy.common.voxelization.VoxelizedSection;
import me.cortex.voxy.common.voxelization.WorldConversionFactory;
import me.cortex.voxy.common.world.WorldEngine;
import me.cortex.voxy.common.world.other.Mipper;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.block.Blocks;
import net.minecraft.nbt.*;
import net.minecraft.network.PacketByteBuf;
import net.minecraft.registry.RegistryKeys;
import net.minecraft.registry.entry.RegistryEntry;
import net.minecraft.util.collection.IndexedIterable;
import net.minecraft.world.World;
import net.minecraft.world.biome.Biome;
import net.minecraft.world.biome.BiomeKeys;
@@ -28,14 +31,16 @@ import java.nio.file.StandardOpenOption;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
public class WorldImporter {
public record ImportUpdate(){}
private final WorldEngine world;
private final World mcWorld;
private final RegistryEntry<Biome> defaultBiome;
private final ReadableContainer<RegistryEntry<Biome>> defaultBiomeProvider;
private final Codec<ReadableContainer<RegistryEntry<Biome>>> biomeCodec;
private final AtomicInteger totalRegions = new AtomicInteger();
private final AtomicInteger regionsProcessed = new AtomicInteger();
@@ -46,7 +51,49 @@ public class WorldImporter {
this.mcWorld = mcWorld;
var biomeRegistry = mcWorld.getRegistryManager().get(RegistryKeys.BIOME);
this.defaultBiome = biomeRegistry.entryOf(BiomeKeys.PLAINS);
var defaultBiome = biomeRegistry.entryOf(BiomeKeys.PLAINS);
this.defaultBiomeProvider = new ReadableContainer<RegistryEntry<Biome>>() {
@Override
public RegistryEntry<Biome> get(int x, int y, int z) {
return defaultBiome;
}
@Override
public void forEachValue(Consumer<RegistryEntry<Biome>> action) {
}
@Override
public void writePacket(PacketByteBuf buf) {
}
@Override
public int getPacketSize() {
return 0;
}
@Override
public boolean hasAny(Predicate<RegistryEntry<Biome>> predicate) {
return false;
}
@Override
public void count(PalettedContainer.Counter<RegistryEntry<Biome>> counter) {
}
@Override
public PalettedContainer<RegistryEntry<Biome>> slice() {
return null;
}
@Override
public Serialized<RegistryEntry<Biome>> serialize(IndexedIterable<RegistryEntry<Biome>> idList, PalettedContainer.PaletteProvider paletteProvider) {
return null;
}
};
this.biomeCodec = PalettedContainer.createReadableContainerCodec(biomeRegistry.getIndexedEntries(), biomeRegistry.createEntryCodec(), PalettedContainer.PaletteProvider.BIOME, biomeRegistry.entryOf(BiomeKeys.PLAINS));
}
@@ -209,7 +256,7 @@ public class WorldImporter {
}
var blockStates = BLOCK_STATE_CODEC.parse(NbtOps.INSTANCE, section.getCompound("block_states")).result().get();
var biomes = this.biomeCodec.parse(NbtOps.INSTANCE, section.getCompound("biomes")).result().orElse(null);
var biomes = this.biomeCodec.parse(NbtOps.INSTANCE, section.getCompound("biomes")).result().orElse(this.defaultBiomeProvider);
VoxelizedSection csec = WorldConversionFactory.convert(
this.world.getMapper(),
blockStates,
@@ -228,10 +275,11 @@ public class WorldImporter {
},
x,
y,
z,
this.defaultBiome
z
);
WorldConversionFactory.mipSection(csec, this.world.getMapper());
this.world.insertUpdate(csec);
while (this.world.savingService.getTaskCount() > 4000) {
try {

View File

@@ -14,14 +14,17 @@ import java.io.File;
public class WorldImportCommand {
public static LiteralArgumentBuilder<FabricClientCommandSource> register() {
return ClientCommandManager.literal("voxy").then(ClientCommandManager.literal("import").then(ClientCommandManager.literal("world").then(ClientCommandManager.argument("world_name", StringArgumentType.string()).executes(WorldImportCommand::importWorld))));
return ClientCommandManager.literal("voxy").then(
ClientCommandManager.literal("import")
.then(ClientCommandManager.literal("world")
.then(ClientCommandManager.argument("world_name", StringArgumentType.string()).executes(WorldImportCommand::importWorld))));
}
public static WorldImporter importerInstance;
private static int importWorld(CommandContext<FabricClientCommandSource> ctx) {
var instance = MinecraftClient.getInstance();
var file = new File(ctx.getArgument("world_name", String.class));
var file = new File("saves").toPath().resolve(ctx.getArgument("world_name", String.class)).resolve("region").toFile();
importerInstance = ((IGetVoxelCore)instance.worldRenderer).getVoxelCore().createWorldImporter(MinecraftClient.getInstance().player.clientWorld, file);
return 0;
}

View File

@@ -0,0 +1,21 @@
package me.cortex.voxy.common.storage.other;
import me.cortex.voxy.common.storage.StorageBackend;
import me.cortex.voxy.common.storage.config.ConfigBuildCtx;
//Very simple config that adds a path to the config builder
public class BasicPathInsertionConfig extends DelegateStorageConfig {
public String path = "";
@Override
public StorageBackend build(ConfigBuildCtx ctx) {
ctx.pushPath(this.path);
var storage = this.delegate.build(ctx);
ctx.popPath();
return storage;
}
public static String getConfigTypeName() {
return "BasicPathConfig";
}
}

View File

@@ -43,20 +43,14 @@ public class CompressionStorageAdaptor extends DelegatingStorageAdaptor {
super.close();
}
public static class Config extends StorageConfig {
public static class Config extends DelegateStorageConfig {
public CompressorConfig compressor;
public StorageConfig delegate;
@Override
public StorageBackend build(ConfigBuildCtx ctx) {
return new CompressionStorageAdaptor(this.compressor.build(ctx), this.delegate.build(ctx));
}
@Override
public List<StorageConfig> getChildStorageConfigs() {
return List.of(this.delegate);
}
public static String getConfigTypeName() {
return "CompressionAdaptor";
}

View File

@@ -0,0 +1,14 @@
package me.cortex.voxy.common.storage.other;
import me.cortex.voxy.common.storage.config.StorageConfig;
import java.util.List;
public abstract class DelegateStorageConfig extends StorageConfig {
public StorageConfig delegate;
@Override
public List<StorageConfig> getChildStorageConfigs() {
return List.of(this.delegate);
}
}

View File

@@ -8,13 +8,9 @@ public class VoxelizedSection {
public final int x;
public final int y;
public final int z;
private final long[] section;
private final long populationMsk;
private final long[][] subSections;
public VoxelizedSection(long[] section, long populationMsk, long[][] subSections, int x, int y, int z) {
final long[] section;
public VoxelizedSection(long[] section, int x, int y, int z) {
this.section = section;
this.populationMsk = populationMsk;
this.subSections = subSections;
this.x = x;
this.y = y;
this.z = z;
@@ -29,31 +25,14 @@ public class VoxelizedSection {
}
public long get(int lvl, int x, int y, int z) {
if (lvl < 2) {
int subIdx = getIdx(x,y,z,2-lvl,2);
var subSec = this.subSections[subIdx];
if (subSec == null) {
return Mapper.AIR;
}
if (lvl == 0) {
return subSec[getIdx(x,y,z,0,2)];
} else if (lvl == 1) {
return subSec[4*4*4+getIdx(x,y,z,0,1)];
}
} else {
if (lvl == 2) {
return section[getIdx(x,y,z,0,2)];
} else if (lvl == 3) {
return section[4*4*4+getIdx(x,y,z,0,1)];
} else if (lvl == 4) {
return section[4*4*4+2*2*2];
}
}
return Mapper.UNKNOWN_MAPPING;
int offset = lvl==1?(1<<12):0;
offset |= lvl==2?(1<<12)|(1<<9):0;
offset |= lvl==3?(1<<12)|(1<<9)|(1<<6):0;
offset |= lvl==4?(1<<12)|(1<<9)|(1<<6)|(1<<3):0;
return this.section[getIdx(x, y, z, 0, 4-lvl) + offset];
}
public static VoxelizedSection createEmpty(int x, int y, int z) {
return new VoxelizedSection(new long[4*4*4+2*2*2+1], 0, new long[4*4*4][], x, y, z);
return new VoxelizedSection(new long[16*16*16 + 8*8*8 + 4*4*4 + 2*2*2 + 1], x, y, z);
}
}

View File

@@ -1,5 +1,6 @@
package me.cortex.voxy.common.voxelization;
import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
import me.cortex.voxy.common.world.other.Mipper;
import me.cortex.voxy.common.world.other.Mapper;
import net.minecraft.block.BlockState;
@@ -9,14 +10,7 @@ import net.minecraft.world.chunk.PalettedContainer;
import net.minecraft.world.chunk.ReadableContainer;
public class WorldConversionFactory {
private static int I(int x, int y, int z) {
return (y<<4)|(z<<2)|x;
}
private static int J(int x, int y, int z) {
return ((y<<2)|(z<<1)|x) + 4*4*4;
}
private static final ThreadLocal<Reference2IntOpenHashMap<BlockState>> BLOCK_CACHE = ThreadLocal.withInitial(Reference2IntOpenHashMap::new);
//TODO: add a local mapper cache since it should be smaller and faster
public static VoxelizedSection convert(Mapper stateMapper,
@@ -25,20 +19,20 @@ public class WorldConversionFactory {
ILightingSupplier lightSupplier,
int sx,
int sy,
int sz,
RegistryEntry<Biome> defaultBiome) {
long[] section = new long[4*4*4+2*2*2+1];//Mipping
long[][] subSections = new long[4*4*4][];
long[] current = new long[4*4*4+2*2*2];
long msk = 0;
int sz) {
var blockCache = BLOCK_CACHE.get();
var section = VoxelizedSection.createEmpty(sx, sy, sz);
var data = section.section;
int blockId = -1;
BlockState block = null;
for (int oy = 0; oy < 4; oy++) {
for (int oz = 0; oz < 4; oz++) {
for (int ox = 0; ox < 4; ox++) {
RegistryEntry<Biome> biome = defaultBiome;
if (biomeContainer != null) {
biome = biomeContainer.get(ox, oy, oz);
}
int nonAir = 0;
int biomeId = stateMapper.getIdForBiome(biomeContainer.get(ox, oy, oz));
for (int iy = 0; iy < 4; iy++) {
for (int iz = 0; iz < 4; iz++) {
for (int ix = 0; ix < 4; ix++) {
@@ -47,61 +41,94 @@ public class WorldConversionFactory {
int z = (oz<<2)|iz;
var state = blockContainer.get(x, y, z);
byte light = lightSupplier.supply(x,y,z,state);
if (!(state.isAir() && (light==0))) {//TODO:FIXME:optimize this in such a way that having skylight access/no skylight means that an entire section is created, WHICH IS VERY BAD FOR PERFORMANCE!!!!
nonAir++;
current[I(ix, iy, iz)] = stateMapper.getBaseId(light, state, biome);
if (!(state.isAir() && (light==0))) {
if (block != state) {
if (state.isAir()) {
block = state;
blockId = 0;
} else {
blockId = blockCache.computeIfAbsent(state, stateMapper::getIdForBlockState);
block = state;
}
}
data[G(x, y, z)] = Mapper.composeMappingId(light, blockId, biomeId);
}
}
}
}
if (nonAir != 0) {
{//Generate mipping
}
}
}
return section;
}
private static int G(int x, int y, int z) {
return ((y<<8)|(z<<4)|x);
}
private static int H(int x, int y, int z) {
return ((y<<6)|(z<<3)|x) + 16*16*16;
}
private static int I(int x, int y, int z) {
return ((y<<4)|(z<<2)|x) + 8*8*8 + 16*16*16;
}
private static int J(int x, int y, int z) {
return ((y<<2)|(z<<1)|x) + 4*4*4 + 8*8*8 + 16*16*16;
}
//TODO: Instead of this mip section as we are updating the data in the world
public static void mipSection(VoxelizedSection section, Mapper mapper) {
var data = section.section;
//Mip L1
int i = 0;
for (int y = 0; y < 4; y += 2) {
for (int z = 0; z < 4; z += 2) {
for (int x = 0; x < 4; x += 2) {
current[4 * 4 * 4 + i++] = Mipper.mip(
current[I(x, y, z)], current[I(x+1, y, z)], current[I(x, y, z+1)], current[I(x+1, y, z+1)],
current[I(x, y+1, z)], current[I(x+1, y+1, z)], current[I(x, y+1, z+1)], current[I(x+1, y+1, z+1)],
stateMapper);
for (int y = 0; y < 16; y+=2) {
for (int z = 0; z < 16; z += 2) {
for (int x = 0; x < 16; x += 2) {
data[16*16*16 + i++] =
Mipper.mip(
data[G(x, y, z)], data[G(x+1, y, z)], data[G(x, y, z+1)], data[G(x+1, y, z+1)],
data[G(x, y+1, z)], data[G(x+1, y+1, z)], data[G(x, y+1, z+1)], data[G(x+1, y+1, z+1)],
mapper);
}
}
}
//Mip L2
section[I(ox, oy, oz)] = Mipper.mip(
current[J(0,0,0)], current[J(1,0,0)], current[J(0,0,1)], current[J(1,0,1)],
current[J(0,1,0)], current[J(1,1,0)], current[J(0,1,1)], current[J(1,1,1)],
stateMapper);
}
//Update existence mask
msk |= 1L<<I(ox, oy, oz);
subSections[I(ox, oy, oz)] = current;
current = new long[4*4*4+2*2*2+1];
}
i = 0;
for (int y = 0; y < 8; y+=2) {
for (int z = 0; z < 8; z += 2) {
for (int x = 0; x < 8; x += 2) {
data[16*16*16 + 8*8*8 + i++] =
Mipper.mip(
data[H(x, y, z)], data[H(x+1, y, z)], data[H(x, y, z+1)], data[H(x+1, y, z+1)],
data[H(x, y+1, z)], data[H(x+1, y+1, z)], data[H(x, y+1, z+1)], data[H(x+1, y+1, z+1)],
mapper);
}
}
}
{//Generate mipping
//Mip L3
int i = 0;
i = 0;
for (int y = 0; y < 4; y+=2) {
for (int z = 0; z < 4; z += 2) {
for (int x = 0; x < 4; x += 2) {
section[4 * 4 * 4 + i++] = Mipper.mip(section[I(x, y, z)], section[I(x+1, y, z)], section[I(x, y, z+1)], section[I(x+1, y, z+1)],
section[I(x, y+1, z)], section[I(x+1, y+1, z)], section[I(x, y+1, z+1)], section[I(x+1, y+1, z+1)],
stateMapper);
data[16*16*16 + 8*8*8 + 4*4*4 + i++] =
Mipper.mip(
data[I(x, y, z)], data[I(x+1, y, z)], data[I(x, y, z+1)], data[I(x+1, y, z+1)],
data[I(x, y+1, z)], data[I(x+1, y+1, z)], data[I(x, y+1, z+1)], data[I(x+1, y+1, z+1)],
mapper);
}
}
}
//Mip L4
section[4*4*4+2*2*2] = Mipper.mip(section[J(0, 0, 0)], section[J(1, 0, 0)], section[J(0, 0, 1)], section[J(1, 0, 1)],
section[J(0, 1, 0)], section[J(1, 1, 0)], section[J(0, 1, 1)], section[J(1, 1, 1)],
stateMapper);
}
return new VoxelizedSection(section, msk, subSections, sx, sy, sz);
//Mip L4
data[16*16*16 + 8*8*8 + 4*4*4 + 2*2*2] =
Mipper.mip(
data[J(0, 0, 0)], data[J(1, 0, 0)], data[J(0, 0, 1)], data[J(1, 0, 1)],
data[J(0, 1, 0)], data[J(1, 1, 0)], data[J(0, 1, 1)], data[J(1, 1, 1)],
mapper);
}
}

View File

@@ -109,7 +109,7 @@ public class WorldEngine {
//TODO: move this to auxilery class so that it can take into account larger than 4 mip levels
//Executes an update to the world and automatically updates all the parent mip layers up to level 4 (e.g. where 1 chunk section is 1 block big)
public void insertUpdate(VoxelizedSection section) {
public void insertUpdate(VoxelizedSection section) {//TODO: add a bitset of levels to update and if it should force update
//The >>1 is cause the world sections size is 32x32x32 vs the 16x16x16 of the voxelized section
for (int lvl = 0; lvl < this.maxMipLevels; lvl++) {
var worldSection = this.acquire(lvl, section.x >> (lvl + 1), section.y >> (lvl + 1), section.z >> (lvl + 1));

View File

@@ -69,10 +69,9 @@ public class VoxelIngestService {
},
chunk.getPos().x,
i,
chunk.getPos().z,
null
chunk.getPos().z
);
WorldConversionFactory.mipSection(csec, this.world.getMapper());
this.world.insertUpdate(csec);
}
}