start 0.5
This commit is contained in:
10
build.gradle
10
build.gradle
@@ -1,5 +1,5 @@
|
||||
plugins {
|
||||
id 'fabric-loom' version "1.8.11"
|
||||
id 'fabric-loom' version "1.10.1"
|
||||
id 'maven-publish'
|
||||
}
|
||||
|
||||
@@ -74,17 +74,17 @@ dependencies {
|
||||
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
|
||||
|
||||
//TODO: this is to eventually not need sodium installed as atm its just used for parsing shaders
|
||||
modRuntimeOnly "maven.modrinth:sodium:mc1.21.4-0.6.10-fabric"
|
||||
modCompileOnly "maven.modrinth:sodium:mc1.21.4-0.6.10-fabric"
|
||||
modRuntimeOnly "maven.modrinth:sodium:mc1.21.5-0.6.12-fabric"
|
||||
modCompileOnly "maven.modrinth:sodium:mc1.21.5-0.6.12-fabric"
|
||||
|
||||
modImplementation("maven.modrinth:lithium:mc1.21.4-0.14.7-fabric")
|
||||
modImplementation("maven.modrinth:lithium:mc1.21.5-0.16.0-fabric")
|
||||
|
||||
//modRuntimeOnly "maven.modrinth:nvidium:0.2.6-beta"
|
||||
modCompileOnly "maven.modrinth:nvidium:0.2.8-beta"
|
||||
|
||||
modImplementation("maven.modrinth:cloth-config:17.0.144+fabric")
|
||||
|
||||
modImplementation("maven.modrinth:modmenu:13.0.0")
|
||||
modImplementation("maven.modrinth:modmenu:14.0.0-rc.2")
|
||||
|
||||
modCompileOnly("maven.modrinth:iris:1.8.5+1.21.4-fabric")
|
||||
|
||||
|
||||
@@ -3,12 +3,12 @@ org.gradle.jvmargs=-Xmx1G
|
||||
|
||||
# Fabric Properties
|
||||
# check these on https://modmuss50.me/fabric.html
|
||||
minecraft_version=1.21.4
|
||||
yarn_mappings=1.21.4+build.8
|
||||
minecraft_version=1.21.5
|
||||
yarn_mappings=1.21.5+build.1
|
||||
loader_version=0.16.10
|
||||
|
||||
# Fabric API
|
||||
fabric_version=0.114.2+1.21.4
|
||||
fabric_version=0.119.5+1.21.5
|
||||
|
||||
# Mod Properties
|
||||
mod_version = 0.2.0-alpha
|
||||
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
|
||||
networkTimeout=10000
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
package me.cortex.voxy.client.core.model;
|
||||
|
||||
import com.mojang.blaze3d.vertex.VertexFormat;
|
||||
import me.cortex.voxy.common.Logger;
|
||||
import net.minecraft.block.BlockEntityProvider;
|
||||
import net.minecraft.block.BlockState;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.client.render.*;
|
||||
import net.minecraft.client.texture.GlTexture;
|
||||
import net.minecraft.client.util.math.MatrixStack;
|
||||
import net.minecraft.util.Identifier;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.util.math.Vec3d;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
@@ -115,11 +119,11 @@ public class BakedBlockEntityModel {
|
||||
System.err.println("ERROR: Empty texture id for layer: " + layer);
|
||||
} else {
|
||||
var texture = MinecraftClient.getInstance().getTextureManager().getTexture(textureId);
|
||||
glBindTexture(GL_TEXTURE_2D, texture.getGlId());
|
||||
glBindTexture(GL_TEXTURE_2D, ((GlTexture)texture.getGlTexture()).getGlId());
|
||||
}
|
||||
}
|
||||
layer.putInto(bb);
|
||||
BufferRenderer.draw(bb.end());
|
||||
BudgetBufferRenderer.draw(bb.end());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,10 +138,9 @@ public class BakedBlockEntityModel {
|
||||
entity.setWorld(MinecraftClient.getInstance().world);
|
||||
if (renderer != null) {
|
||||
try {
|
||||
renderer.render(entity, 0.0f, new MatrixStack(), layer->map.computeIfAbsent(layer, BakedVertices::new), 0, 0);
|
||||
renderer.render(entity, 0.0f, new MatrixStack(), layer->map.computeIfAbsent(layer, BakedVertices::new), 0, 0, new Vec3d(0,0,0));
|
||||
} catch (Exception e) {
|
||||
System.err.println("Unable to bake block entity: " + entity);
|
||||
e.printStackTrace();
|
||||
Logger.error("Unable to bake block entity: " + entity, e);
|
||||
}
|
||||
}
|
||||
entity.markRemoved();
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package me.cortex.voxy.client.core.model;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.VertexFormat;
|
||||
import net.minecraft.client.gl.GlGpuBuffer;
|
||||
import net.minecraft.client.render.BuiltBuffer;
|
||||
|
||||
import static org.lwjgl.opengl.GL15C.*;
|
||||
|
||||
public class BudgetBufferRenderer {
|
||||
public static void draw(BuiltBuffer buffer) {
|
||||
var params = buffer.getDrawParameters();
|
||||
try (var gpuBuf = params.format().uploadImmediateIndexBuffer(buffer.getBuffer())) {
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ((GlGpuBuffer)RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.QUADS).getIndexBuffer(params.indexCount())).id);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
package me.cortex.voxy.client.core.model;
|
||||
|
||||
import com.mojang.blaze3d.platform.GlConst;
|
||||
import com.mojang.blaze3d.platform.GlStateManager;
|
||||
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
|
||||
@@ -597,7 +595,7 @@ public class ModelFactory {
|
||||
private float[] computeModelDepth(ColourDepthTextureData[] textures, int checkMode) {
|
||||
float[] res = new float[6];
|
||||
for (var dir : Direction.values()) {
|
||||
var data = textures[dir.getId()];
|
||||
var data = textures[dir.getIndex()];
|
||||
float fd = TextureUtils.computeDepth(data, TextureUtils.DEPTH_MODE_AVG, checkMode);//Compute the min float depth, smaller means closer to the camera, range 0-1
|
||||
int depth = Math.round(fd * MODEL_TEXTURE_SIZE);
|
||||
//If fd is -1, it means that there was nothing rendered on that face and it should be discarded
|
||||
@@ -642,10 +640,10 @@ public class ModelFactory {
|
||||
int x = X + (subTex>>1)*MODEL_TEXTURE_SIZE;
|
||||
int y = Y + (subTex&1)*MODEL_TEXTURE_SIZE;
|
||||
|
||||
GlStateManager._pixelStore(GlConst.GL_UNPACK_ROW_LENGTH, 0);
|
||||
GlStateManager._pixelStore(GlConst.GL_UNPACK_SKIP_PIXELS, 0);
|
||||
GlStateManager._pixelStore(GlConst.GL_UNPACK_SKIP_ROWS, 0);
|
||||
GlStateManager._pixelStore(GlConst.GL_UNPACK_ALIGNMENT, 4);
|
||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||
glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0);
|
||||
glPixelStorei(GL_UNPACK_SKIP_ROWS, 0);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||
var current = textures[subTex].colour();
|
||||
var next = new int[current.length>>1];
|
||||
final int layers = Integer.numberOfTrailingZeros(MODEL_TEXTURE_SIZE);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package me.cortex.voxy.client.core.model;
|
||||
|
||||
import com.mojang.blaze3d.platform.GlStateManager;
|
||||
import com.mojang.blaze3d.vertex.VertexFormat;
|
||||
import me.cortex.voxy.client.core.gl.GlFramebuffer;
|
||||
import me.cortex.voxy.client.core.gl.GlTexture;
|
||||
import me.cortex.voxy.client.core.gl.shader.Shader;
|
||||
@@ -11,7 +11,7 @@ import net.minecraft.block.entity.BlockEntity;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.client.gl.GlUniform;
|
||||
import net.minecraft.client.render.*;
|
||||
import net.minecraft.client.render.model.BakedModel;
|
||||
import net.minecraft.client.render.model.BlockStateModel;
|
||||
import net.minecraft.client.util.BufferAllocator;
|
||||
import net.minecraft.client.util.math.MatrixStack;
|
||||
import net.minecraft.fluid.FluidState;
|
||||
@@ -188,7 +188,7 @@ public class ModelTextureBakery {
|
||||
glStencilFunc(GL_ALWAYS, 1, 0xFF);
|
||||
glStencilMask(0xFF);
|
||||
|
||||
int texId = MinecraftClient.getInstance().getTextureManager().getTexture(Identifier.of("minecraft", "textures/atlas/blocks.png")).getGlId();
|
||||
int texId = ((net.minecraft.client.texture.GlTexture)MinecraftClient.getInstance().getTextureManager().getTexture(Identifier.of("minecraft", "textures/atlas/blocks.png")).getGlTexture()).getGlId();
|
||||
|
||||
final int TEXTURE_SIZE = this.width*this.height *4;//NOTE! assume here that both depth and colour are 4 bytes in size
|
||||
for (int i = 0; i < FACE_VIEWS.size(); i++) {
|
||||
@@ -217,10 +217,10 @@ public class ModelTextureBakery {
|
||||
}
|
||||
|
||||
private final BufferAllocator allocator = new BufferAllocator(786432);
|
||||
private void captureViewToStream(BlockState state, BakedModel model, BakedBlockEntityModel blockEntityModel, MatrixStack stack, long randomValue, int face, boolean renderFluid, int textureId, Matrix4f projection, int streamBuffer, int streamOffset) {
|
||||
private void captureViewToStream(BlockState state, BlockStateModel model, BakedBlockEntityModel blockEntityModel, MatrixStack stack, long randomValue, int face, boolean renderFluid, int textureId, Matrix4f projection, int streamBuffer, int streamOffset) {
|
||||
this.rasterShader.bind();
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
GlUniform.uniform1(0, 0);
|
||||
glUniform1i(0, 0);
|
||||
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||||
float[] mat = new float[4*4];
|
||||
@@ -251,7 +251,7 @@ public class ModelTextureBakery {
|
||||
if (!renderFluid) {
|
||||
//TODO: need to do 2 variants for quads, one which have coloured, ones that dont, might be able to pull a spare bit
|
||||
// at the end whether or not a pixel should be mixed with texture
|
||||
renderQuads(bb, state, model, new MatrixStack(), randomValue);
|
||||
renderQuads(bb, model, new MatrixStack(), randomValue);
|
||||
} else {
|
||||
MinecraftClient.getInstance().getBlockRenderManager().renderFluid(BlockPos.ORIGIN, new BlockRenderView() {
|
||||
@Override
|
||||
@@ -282,7 +282,7 @@ public class ModelTextureBakery {
|
||||
|
||||
@Override
|
||||
public BlockState getBlockState(BlockPos pos) {
|
||||
if (pos.equals(Direction.byId(face).getVector())) {
|
||||
if (pos.equals(Direction.byIndex(face).getVector())) {
|
||||
return Blocks.AIR.getDefaultState();
|
||||
}
|
||||
|
||||
@@ -300,7 +300,7 @@ public class ModelTextureBakery {
|
||||
|
||||
@Override
|
||||
public FluidState getFluidState(BlockPos pos) {
|
||||
if (pos.equals(Direction.byId(face).getVector())) {
|
||||
if (pos.equals(Direction.byIndex(face).getVector())) {
|
||||
return Blocks.AIR.getDefaultState().getFluidState();
|
||||
}
|
||||
//if (pos.getY() == 1) {
|
||||
@@ -324,7 +324,7 @@ public class ModelTextureBakery {
|
||||
glBindTexture(GL_TEXTURE_2D, textureId);
|
||||
try {
|
||||
//System.err.println("REPLACE THE UPLOADING WITH THREAD SAFE VARIENT");
|
||||
BufferRenderer.draw(bb.end());
|
||||
BudgetBufferRenderer.draw(bb.end());
|
||||
} catch (IllegalStateException e) {
|
||||
//System.err.println("Got empty buffer builder! for block " + state);
|
||||
}
|
||||
@@ -356,13 +356,15 @@ public class ModelTextureBakery {
|
||||
glDispatchCompute(1,1,1);
|
||||
}
|
||||
|
||||
private static void renderQuads(BufferBuilder builder, BlockState state, BakedModel model, MatrixStack stack, long randomValue) {
|
||||
private static void renderQuads(BufferBuilder builder, BlockStateModel model, MatrixStack stack, long randomValue) {
|
||||
for (Direction direction : new Direction[]{Direction.DOWN, Direction.UP, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST, null}) {
|
||||
var quads = model.getQuads(state, direction, new LocalRandom(randomValue));
|
||||
for (var quad : quads) {
|
||||
//TODO: mark pixels that have
|
||||
int meta = 1;
|
||||
builder.quad(stack.peek(), quad, ((meta>>16)&0xff)/255f, ((meta>>8)&0xff)/255f, (meta&0xff)/255f, 1.0f, 0, 0);
|
||||
for (var part : model.getParts(new LocalRandom(randomValue))) {
|
||||
var quads = part.getQuads(direction);
|
||||
for (var quad : quads) {
|
||||
//TODO: mark pixels that have
|
||||
int meta = 1;
|
||||
builder.quad(stack.peek(), quad, ((meta >> 16) & 0xff) / 255f, ((meta >> 8) & 0xff) / 255f, (meta & 0xff) / 255f, 1.0f, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package me.cortex.voxy.client.core.rendering;
|
||||
|
||||
import me.cortex.voxy.client.core.gl.GlBuffer;
|
||||
import me.cortex.voxy.client.core.gl.GlTexture;
|
||||
import me.cortex.voxy.client.core.rendering.util.UploadStream;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
@@ -36,6 +37,6 @@ public class LightMapHelper {
|
||||
public static void bind(int lightingIndex) {
|
||||
//glBindBufferBase(GL_SHADER_STORAGE_BUFFER, lightingBufferIndex, LIGHT_MAP_BUFFER.id);
|
||||
//glBindSampler(lightingIndex, SAMPLER);
|
||||
glBindTextureUnit(lightingIndex, MinecraftClient.getInstance().gameRenderer.getLightmapTextureManager().lightmapFramebuffer.getColorAttachment());
|
||||
glBindTextureUnit(lightingIndex, ((net.minecraft.client.texture.GlTexture)(MinecraftClient.getInstance().gameRenderer.getLightmapTextureManager().getGlTexture())).getGlId());
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,7 @@ public class VoxyRenderSystem {
|
||||
var client = MinecraftClient.getInstance();
|
||||
var gameRenderer = client.gameRenderer;//tickCounter.getTickDelta(true);
|
||||
|
||||
float fov = gameRenderer.getFov(gameRenderer.getCamera(), client.getRenderTickCounter().getTickDelta(true), true);
|
||||
float fov = gameRenderer.getFov(gameRenderer.getCamera(), client.getRenderTickCounter().getTickProgress(true), true);
|
||||
|
||||
projection.setPerspective(fov * 0.01745329238474369f,
|
||||
(float) client.getWindow().getFramebufferWidth() / (float)client.getWindow().getFramebufferHeight(),
|
||||
|
||||
@@ -313,10 +313,10 @@ public class Mapper {
|
||||
public static StateEntry deserialize(int id, byte[] data) {
|
||||
try {
|
||||
var compound = NbtIo.readCompressed(new ByteArrayInputStream(data), NbtSizeTracker.ofUnlimitedBytes());
|
||||
if (compound.getInt("id") != id) {
|
||||
if (compound.getInt("id", -1) != id) {
|
||||
throw new IllegalStateException("Encoded id != expected id");
|
||||
}
|
||||
BlockState state = BlockState.CODEC.parse(NbtOps.INSTANCE, compound.getCompound("block_state")).getOrThrow();
|
||||
BlockState state = BlockState.CODEC.parse(NbtOps.INSTANCE, compound.getCompound("block_state").orElseThrow()).getOrThrow();
|
||||
return new StateEntry(id, state);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
@@ -349,10 +349,10 @@ public class Mapper {
|
||||
public static BiomeEntry deserialize(int id, byte[] data) {
|
||||
try {
|
||||
var compound = NbtIo.readCompressed(new ByteArrayInputStream(data), NbtSizeTracker.ofUnlimitedBytes());
|
||||
if (compound.getInt("id") != id) {
|
||||
if (compound.getInt("id", -1) != id) {
|
||||
throw new IllegalStateException("Encoded id != expected id");
|
||||
}
|
||||
String biome = compound.getString("biome_id");
|
||||
String biome = compound.getString("biome_id", null);
|
||||
return new BiomeEntry(id, biome);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
|
||||
@@ -396,22 +396,22 @@ public class WorldImporter implements IDataImporter {
|
||||
}
|
||||
|
||||
//Dont process non full chunk sections
|
||||
var status = ChunkStatus.byId(chunk.getString("Status"));
|
||||
var status = ChunkStatus.byId(chunk.getString("Status", null));
|
||||
if (status != ChunkStatus.FULL && status != ChunkStatus.EMPTY) {//We also import empty since they are from data upgrade
|
||||
this.totalChunks.decrementAndGet();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
int x = chunk.getInt("xPos");
|
||||
int z = chunk.getInt("zPos");
|
||||
int x = chunk.getInt("xPos", Integer.MIN_VALUE);
|
||||
int z = chunk.getInt("zPos", Integer.MIN_VALUE);
|
||||
if (x>>5 != regionX || z>>5 != regionZ) {
|
||||
Logger.error("Chunk position is not located in correct region, expected: (" + regionX + ", " + regionZ+"), got: " + "(" + (x>>5) + ", " + (z>>5)+"), importing anyway");
|
||||
}
|
||||
|
||||
for (var sectionE : chunk.getList("sections", NbtElement.COMPOUND_TYPE)) {
|
||||
for (var sectionE : chunk.getList("sections").orElseThrow()) {
|
||||
var section = (NbtCompound) sectionE;
|
||||
int y = section.getInt("Y");
|
||||
int y = section.getInt("Y", Integer.MIN_VALUE);
|
||||
this.importSectionNBT(x, y, z, section);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@@ -421,6 +421,7 @@ public class WorldImporter implements IDataImporter {
|
||||
this.updateCallback.onUpdate(this.chunksProcessed.incrementAndGet(), this.estimatedTotalChunks.get());
|
||||
}
|
||||
|
||||
private static final byte[] EMPTY = new byte[0];
|
||||
private static final ThreadLocal<VoxelizedSection> SECTION_CACHE = ThreadLocal.withInitial(VoxelizedSection::createEmpty);
|
||||
private static final Codec<PalettedContainer<BlockState>> BLOCK_STATE_CODEC = PalettedContainer.createPalettedContainerCodec(Block.STATE_IDS, BlockState.CODEC, PalettedContainer.PaletteProvider.BLOCK_STATE, Blocks.AIR.getDefaultState());
|
||||
private void importSectionNBT(int x, int y, int z, NbtCompound section) {
|
||||
@@ -428,8 +429,8 @@ public class WorldImporter implements IDataImporter {
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] blockLightData = section.getByteArray("BlockLight");
|
||||
byte[] skyLightData = section.getByteArray("SkyLight");
|
||||
byte[] blockLightData = section.getByteArray("BlockLight").orElse(EMPTY);
|
||||
byte[] skyLightData = section.getByteArray("SkyLight").orElse(EMPTY);
|
||||
|
||||
ChunkNibbleArray blockLight;
|
||||
if (blockLightData.length != 0) {
|
||||
@@ -445,13 +446,13 @@ public class WorldImporter implements IDataImporter {
|
||||
skyLight = null;
|
||||
}
|
||||
|
||||
var blockStatesRes = BLOCK_STATE_CODEC.parse(NbtOps.INSTANCE, section.getCompound("block_states"));
|
||||
var blockStatesRes = BLOCK_STATE_CODEC.parse(NbtOps.INSTANCE, section.getCompound("block_states").get());
|
||||
if (!blockStatesRes.hasResultOrPartial()) {
|
||||
//TODO: if its only partial, it means should try to upgrade the nbt format with datafixerupper probably
|
||||
return;
|
||||
}
|
||||
var blockStates = blockStatesRes.getPartialOrThrow();
|
||||
var biomes = this.biomeCodec.parse(NbtOps.INSTANCE, section.getCompound("biomes")).result().orElse(this.defaultBiomeProvider);
|
||||
var biomes = this.biomeCodec.parse(NbtOps.INSTANCE, section.getCompound("biomes").get()).result().orElse(this.defaultBiomeProvider);
|
||||
|
||||
VoxelizedSection csec = WorldConversionFactory.convert(
|
||||
SECTION_CACHE.get().setPosition(x, y, z),
|
||||
|
||||
@@ -21,8 +21,8 @@ accessible field net/minecraft/client/network/ClientPlayerInteractionManager net
|
||||
accessible method net/minecraft/client/render/GameRenderer getFov (Lnet/minecraft/client/render/Camera;FZ)F
|
||||
accessible method net/minecraft/client/render/RenderPhase$TextureBase getId ()Ljava/util/Optional;
|
||||
|
||||
accessible field net/minecraft/client/render/LightmapTextureManager lightmapFramebuffer Lnet/minecraft/client/gl/SimpleFramebuffer;
|
||||
|
||||
accessible field net/minecraft/world/chunk/PalettedContainer data Lnet/minecraft/world/chunk/PalettedContainer$Data;
|
||||
accessible field net/minecraft/world/chunk/PalettedContainer$Data storage Lnet/minecraft/util/collection/PaletteStorage;
|
||||
accessible field net/minecraft/world/chunk/PalettedContainer$Data palette Lnet/minecraft/world/chunk/Palette;
|
||||
accessible field net/minecraft/world/chunk/PalettedContainer$Data palette Lnet/minecraft/world/chunk/Palette;
|
||||
|
||||
accessible field net/minecraft/client/gl/GlGpuBuffer id I
|
||||
Reference in New Issue
Block a user