diff --git a/src/api/java/net/caffeinemc/mods/sodium/api/buffer/UnmanagedBufferBuilder.java b/src/api/java/net/caffeinemc/mods/sodium/api/buffer/UnmanagedBufferBuilder.java new file mode 100644 index 0000000000..25ac2b8221 --- /dev/null +++ b/src/api/java/net/caffeinemc/mods/sodium/api/buffer/UnmanagedBufferBuilder.java @@ -0,0 +1,70 @@ +package net.caffeinemc.mods.sodium.api.buffer; + +import net.caffeinemc.mods.sodium.api.memory.MemoryIntrinsics; +import net.minecraft.client.util.GlAllocationUtils; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.system.MemoryUtil; + +import java.nio.ByteBuffer; + +/** + * Provides a growing buffer with a push method for convenience. + * Otherwise, this class is very un-managed. + */ +public class UnmanagedBufferBuilder { + private ByteBuffer buffer; + private int byteOffset = 0; + + public UnmanagedBufferBuilder(int initialCapacity) { + this.buffer = GlAllocationUtils.allocateByteBuffer(initialCapacity); + } + + public void ensureCapacity(int capacity) { + if (capacity > this.buffer.capacity()) { + int newCapacity = (int) Math.ceil(this.buffer.capacity() * 1.5); + ByteBuffer byteBuffer = GlAllocationUtils.resizeByteBuffer(this.buffer, Math.max(newCapacity, capacity)); + byteBuffer.rewind(); + this.buffer = byteBuffer; + } + } + + /** + * Copies memory from the stack onto the end of this buffer builder. + */ + public void push(MemoryStack ignoredStack, long src, int size) { + ensureCapacity(byteOffset + size); + + long dst = MemoryUtil.memAddress(this.buffer, this.byteOffset); + MemoryIntrinsics.copyMemory(src, dst, size); + byteOffset += size; + } + + public Built build() { + return new Built(this.byteOffset, MemoryUtil.memSlice(this.buffer, 0, this.byteOffset)); + } + + public void reset() { + this.byteOffset = 0; + } + + /** + * Builds and resets this builder. + * Make sure to use/upload the return value before pushing more data. + * @return a built buffer containing all the data pushed to this builder + */ + public Built end() { + int endOffset = this.byteOffset; + this.byteOffset = 0; + return new Built(endOffset, MemoryUtil.memSlice(this.buffer, 0, endOffset)); + } + + public static class Built { + public int size; + public ByteBuffer buffer; + + Built(int size, ByteBuffer buffer) { + this.size = size; + this.buffer = buffer; + } + } +} diff --git a/src/api/java/net/caffeinemc/mods/sodium/api/util/RawUVs.java b/src/api/java/net/caffeinemc/mods/sodium/api/util/RawUVs.java new file mode 100644 index 0000000000..784ece5152 --- /dev/null +++ b/src/api/java/net/caffeinemc/mods/sodium/api/util/RawUVs.java @@ -0,0 +1,50 @@ +package net.caffeinemc.mods.sodium.api.util; + +import org.lwjgl.system.MemoryUtil; + +public record RawUVs(float minU, float minV, float maxU, float maxV) { + public static final int STRIDE = 16; + + public void put(long ptr) { + MemoryUtil.memPutFloat(ptr + 0, minU); + MemoryUtil.memPutFloat(ptr + 4, minV); + MemoryUtil.memPutFloat(ptr + 8, maxU); + MemoryUtil.memPutFloat(ptr + 12, maxV); + } + + public static void putNull(long ptr) { + MemoryUtil.memPutFloat(ptr + 0, Float.NaN); + MemoryUtil.memPutFloat(ptr + 4, Float.NaN); + MemoryUtil.memPutFloat(ptr + 8, Float.NaN); + MemoryUtil.memPutFloat(ptr + 12, Float.NaN); + } + + public long key() { + return ((long) Float.floatToRawIntBits(minU)) << 32 | Float.floatToRawIntBits(minV); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o instanceof RawUVs other) { + return Float.floatToRawIntBits(minU) == Float.floatToRawIntBits(other.minU()) && + Float.floatToRawIntBits(minV) == Float.floatToRawIntBits(other.minV()) && + Float.floatToRawIntBits(maxU) == Float.floatToRawIntBits(other.maxU()) && + Float.floatToRawIntBits(maxV) == Float.floatToRawIntBits(other.maxV()); + } else { + return false; + } + } + + @Override + public int hashCode() { + int result = 1; + + result = 31 * result + Float.floatToRawIntBits(minU); + result = 31 * result + Float.floatToRawIntBits(minV); + result = 31 * result + Float.floatToRawIntBits(maxU); + result = 31 * result + Float.floatToRawIntBits(maxV); + + return result; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/arena/staging/StagingBufferBuilder.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/arena/staging/StagingBufferBuilder.java new file mode 100644 index 0000000000..e5690e9779 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/arena/staging/StagingBufferBuilder.java @@ -0,0 +1,76 @@ +package me.jellysquid.mods.sodium.client.gl.arena.staging; + +import me.jellysquid.mods.sodium.client.SodiumClientMod; +import me.jellysquid.mods.sodium.client.gl.buffer.*; +import me.jellysquid.mods.sodium.client.gl.device.CommandList; +import me.jellysquid.mods.sodium.client.gl.util.EnumBitField; +import org.lwjgl.system.MemoryStack; + +public class StagingBufferBuilder { + private static final EnumBitField STORAGE_FLAGS = + EnumBitField.of(GlBufferStorageFlags.PERSISTENT, GlBufferStorageFlags.CLIENT_STORAGE, GlBufferStorageFlags.MAP_WRITE); + + private static final EnumBitField MAP_FLAGS = + EnumBitField.of(GlBufferMapFlags.PERSISTENT, GlBufferMapFlags.INVALIDATE_BUFFER, GlBufferMapFlags.WRITE, GlBufferMapFlags.EXPLICIT_FLUSH); + + private final MappedBuffer mappedBuffer; + + private int capacity; + + private int start = 0; + private int pos = 0; + + private int numFrames = 0; + + public StagingBufferBuilder(CommandList commandList, int capacity) { + this.capacity = capacity; + + GlImmutableBuffer buffer = commandList.createImmutableBuffer(capacity, STORAGE_FLAGS); + GlBufferMapping map = commandList.mapBuffer(buffer, 0, capacity, MAP_FLAGS); + + this.mappedBuffer = new MappedBuffer(buffer, map); + } + + public void push(MemoryStack stack, long ptr, int size) { + if (pos + size > this.capacity) { + // TODO: Needs a fallback instead of throwing, (even if it is highly unlikely) + + throw new IllegalStateException("Particle data buffer overflowed!\n" + + "Capacity: " + this.capacity); + } + + this.mappedBuffer.map.write(stack, ptr, size, pos); + pos += size; + } + + public void endAndUpload(CommandList commandList, GlBuffer dst, long writeOffset) { + this.flush(commandList, dst, this.start, this.pos - this.start, writeOffset); + this.start = this.pos; + } + + public void flipFrame() { + this.numFrames++; + if (numFrames >= SodiumClientMod.options().advanced.cpuRenderAheadLimit) { + this.reset(); + } + } + + private void reset() { + this.start = 0; + this.pos = 0; + this.numFrames = 0; + } + + private void flush(CommandList commandList, GlBuffer dst, int readOffset, int length, long writeOffset) { + commandList.flushMappedRange(this.mappedBuffer.map, readOffset, length); + commandList.copyBufferSubData(this.mappedBuffer.buffer, dst, readOffset, writeOffset, length); + } + + private record MappedBuffer(GlImmutableBuffer buffer, + GlBufferMapping map) { + public void delete(CommandList commandList) { + commandList.unmap(this.map); + commandList.deleteBuffer(this.buffer); + } + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/buffer/GlBufferMapping.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/buffer/GlBufferMapping.java index f7288f8ab7..6cb4126c03 100644 --- a/src/main/java/me/jellysquid/mods/sodium/client/gl/buffer/GlBufferMapping.java +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/buffer/GlBufferMapping.java @@ -1,5 +1,6 @@ package me.jellysquid.mods.sodium.client.gl.buffer; +import org.lwjgl.system.MemoryStack; import org.lwjgl.system.MemoryUtil; import java.nio.ByteBuffer; @@ -19,6 +20,10 @@ public void write(ByteBuffer data, int writeOffset) { MemoryUtil.memCopy(MemoryUtil.memAddress(data), MemoryUtil.memAddress(this.map, writeOffset), data.remaining()); } + public void write(MemoryStack ignoredStack, long ptr, int size, int writeOffset) { + MemoryUtil.memCopy(ptr, MemoryUtil.memAddress(this.map, writeOffset), size); + } + public GlBuffer getBufferObject() { return this.buffer; } diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/buffer/GlBufferTexture.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/buffer/GlBufferTexture.java new file mode 100644 index 0000000000..8354ea01ae --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/buffer/GlBufferTexture.java @@ -0,0 +1,33 @@ +package me.jellysquid.mods.sodium.client.gl.buffer; + +import com.mojang.blaze3d.platform.GlStateManager; +import com.mojang.blaze3d.systems.RenderSystem; +import me.jellysquid.mods.sodium.client.gl.device.CommandList; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL31; + +import java.nio.ByteBuffer; + +public class GlBufferTexture { + private final GlBuffer buffer; + + private final int glTexHandle; + + private final int textureNum; + + public GlBufferTexture(GlBuffer buffer, int textureNum) { + this.buffer = buffer; + this.glTexHandle = GlStateManager._genTexture(); + this.textureNum = textureNum; + } + + public int getTextureNum() { + return textureNum; + } + + public void bind() { + GL11.glBindTexture(GL31.GL_TEXTURE_BUFFER, this.glTexHandle); + GL31.glTexBuffer(GL31.GL_TEXTURE_BUFFER, GL31.GL_R32UI, this.buffer.handle()); + RenderSystem.setShaderTexture(this.textureNum, this.glTexHandle); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/buffer/GlContinuousUploadBuffer.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/buffer/GlContinuousUploadBuffer.java new file mode 100644 index 0000000000..a678e7fcfd --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/buffer/GlContinuousUploadBuffer.java @@ -0,0 +1,51 @@ +package me.jellysquid.mods.sodium.client.gl.buffer; + +import me.jellysquid.mods.sodium.client.SodiumClientMod; +import me.jellysquid.mods.sodium.client.gl.arena.staging.FallbackStagingBuffer; +import me.jellysquid.mods.sodium.client.gl.arena.staging.MappedStagingBuffer; +import me.jellysquid.mods.sodium.client.gl.arena.staging.StagingBuffer; +import me.jellysquid.mods.sodium.client.gl.device.CommandList; +import me.jellysquid.mods.sodium.client.gl.device.RenderDevice; + +import java.nio.ByteBuffer; + +public class GlContinuousUploadBuffer extends GlMutableBuffer { + private static final GlBufferUsage BUFFER_USAGE = GlBufferUsage.STATIC_DRAW; + private static final int DEFAULT_INITIAL_CAPACITY = 1024; + + private final StagingBuffer stagingBuffer; + + private int capacity; + + public GlContinuousUploadBuffer(CommandList commands, int initialCapacity) { + super(); + this.capacity = initialCapacity; + this.stagingBuffer = createStagingBuffer(commands); + commands.allocateStorage(this, this.capacity, BUFFER_USAGE); + } + + public GlContinuousUploadBuffer(CommandList commands) { + this(commands, DEFAULT_INITIAL_CAPACITY); + } + + public void uploadOverwrite(CommandList commandList, ByteBuffer data, int size) { + ensureCapacity(commandList, size); + this.stagingBuffer.enqueueCopy(commandList, data, this, 0); + this.stagingBuffer.flush(commandList); + } + + public void ensureCapacity(CommandList commandList, int capacity) { + if (capacity > this.capacity) { + this.capacity = capacity; + commandList.allocateStorage(this, capacity, BUFFER_USAGE); + } + } + + private static StagingBuffer createStagingBuffer(CommandList commandList) { + if (SodiumClientMod.options().advanced.useAdvancedStagingBuffers && MappedStagingBuffer.isSupported(RenderDevice.INSTANCE)) { + return new MappedStagingBuffer(commandList); + } + + return new FallbackStagingBuffer(commandList); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/particle/BillboardExtended.java b/src/main/java/me/jellysquid/mods/sodium/client/render/particle/BillboardExtended.java new file mode 100644 index 0000000000..f7e3ec6cf5 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/particle/BillboardExtended.java @@ -0,0 +1,10 @@ +package me.jellysquid.mods.sodium.client.render.particle; + +import me.jellysquid.mods.sodium.client.gl.arena.staging.StagingBufferBuilder; +import me.jellysquid.mods.sodium.client.render.particle.cache.ParticleTextureCache; +import net.caffeinemc.mods.sodium.api.buffer.UnmanagedBufferBuilder; +import net.minecraft.client.render.Camera; + +public interface BillboardExtended { + void sodium$buildParticleData(StagingBufferBuilder builder, ParticleTextureCache registry, Camera camera, float tickDelta); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/particle/ParticleBuffers.java b/src/main/java/me/jellysquid/mods/sodium/client/render/particle/ParticleBuffers.java new file mode 100644 index 0000000000..d45912b2f2 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/particle/ParticleBuffers.java @@ -0,0 +1,37 @@ +package me.jellysquid.mods.sodium.client.render.particle; + +import me.jellysquid.mods.sodium.client.gl.arena.staging.StagingBufferBuilder; +import me.jellysquid.mods.sodium.client.gl.buffer.GlBufferTexture; +import me.jellysquid.mods.sodium.client.gl.buffer.GlBufferUsage; +import me.jellysquid.mods.sodium.client.gl.buffer.GlContinuousUploadBuffer; +import me.jellysquid.mods.sodium.client.gl.buffer.GlMutableBuffer; +import me.jellysquid.mods.sodium.client.gl.device.CommandList; +import net.caffeinemc.mods.sodium.api.buffer.UnmanagedBufferBuilder; + +public class ParticleBuffers { + private final GlMutableBuffer particleData; + private final GlContinuousUploadBuffer textureCache; + + private final GlBufferTexture particleDataTex; + private final GlBufferTexture textureCacheTex; + + public ParticleBuffers(CommandList commandList) { + this.particleData = commandList.createMutableBuffer(); + commandList.allocateStorage(particleData, 3 * 16384 * 32, GlBufferUsage.STATIC_DRAW); + + this.textureCache = new GlContinuousUploadBuffer(commandList); + + this.particleDataTex = new GlBufferTexture(particleData, 3); + this.textureCacheTex = new GlBufferTexture(textureCache, 4); + } + + public void uploadParticleData(CommandList commandList, StagingBufferBuilder data, UnmanagedBufferBuilder.Built cache) { + data.endAndUpload(commandList, this.particleData, 0); + this.textureCache.uploadOverwrite(commandList, cache.buffer, cache.size); + } + + public void bind() { + this.particleDataTex.bind(); + this.textureCacheTex.bind(); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/particle/ParticleExtended.java b/src/main/java/me/jellysquid/mods/sodium/client/render/particle/ParticleExtended.java new file mode 100644 index 0000000000..731e6b439a --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/particle/ParticleExtended.java @@ -0,0 +1,5 @@ +package me.jellysquid.mods.sodium.client.render.particle; + +public interface ParticleExtended { + void sodium$configure(ParticleRenderView renderView); +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/particle/ParticleRenderView.java b/src/main/java/me/jellysquid/mods/sodium/client/render/particle/ParticleRenderView.java new file mode 100644 index 0000000000..7ce3146bf8 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/particle/ParticleRenderView.java @@ -0,0 +1,83 @@ +package me.jellysquid.mods.sodium.client.render.particle; + +import it.unimi.dsi.fastutil.longs.Long2ReferenceLinkedOpenHashMap; +import net.minecraft.client.render.LightmapTextureManager; +import net.minecraft.util.math.ChunkSectionPos; +import net.minecraft.world.LightType; +import net.minecraft.world.World; +import net.minecraft.world.chunk.ChunkNibbleArray; +import org.jetbrains.annotations.NotNull; + +public class ParticleRenderView { + private static final int CACHE_SIZE = 2048; + + private final Long2ReferenceLinkedOpenHashMap lightCache = new Long2ReferenceLinkedOpenHashMap<>(CACHE_SIZE); + private final World world; + + public ParticleRenderView(World world) { + this.world = world; + } + + public int getBrightness(int x, int y, int z) { + var arrays = this.getCachedLightArrays(x >> 4, y >> 4, z >> 4); + + int skyLight = getLightValue(arrays.skyLight, x, y, z); + int blockLight = getLightValue(arrays.blockLight, x, y, z); + + return LightmapTextureManager.pack(blockLight, skyLight); + } + + private static int getLightValue(ChunkNibbleArray array, int x, int y, int z) { + return array == null ? 0 : array.get(x & 15, y & 15, z & 15); + } + + private LightArrays getCachedLightArrays(int x, int y, int z) { + var position = ChunkSectionPos.asLong(x, y, z); + + LightArrays entry = this.lightCache.get(position); + + if (entry == null) { + entry = this.fetchLightArrays(x, y, z, position); + } + + return entry; + } + + private ParticleRenderView.@NotNull LightArrays fetchLightArrays(int x, int y, int z, long packed) { + LightArrays arrays = LightArrays.load(this.world, ChunkSectionPos.from(x, y, z)); + + if (this.lightCache.size() >= CACHE_SIZE) { + this.lightCache.removeLast(); + } + + this.lightCache.putAndMoveToFirst(packed, arrays); + + return arrays; + } + + public void resetCache() { + this.lightCache.clear(); + } + + private static class LightArrays { + private final ChunkNibbleArray blockLight; + private final ChunkNibbleArray skyLight; + + private LightArrays(ChunkNibbleArray blockLight, ChunkNibbleArray skyLight) { + this.blockLight = blockLight; + this.skyLight = skyLight; + } + + public static LightArrays load(World world, ChunkSectionPos pos) { + var lightingProvider = world.getLightingProvider(); + + var blockLight = lightingProvider.get(LightType.BLOCK) + .getLightSection(pos); + + var skyLight = lightingProvider.get(LightType.SKY) + .getLightSection(pos); + + return new LightArrays(blockLight, skyLight); + } + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/particle/ShaderBillboardParticleRenderer.java b/src/main/java/me/jellysquid/mods/sodium/client/render/particle/ShaderBillboardParticleRenderer.java new file mode 100644 index 0000000000..95a3c2b943 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/particle/ShaderBillboardParticleRenderer.java @@ -0,0 +1,51 @@ +package me.jellysquid.mods.sodium.client.render.particle; + +import me.jellysquid.mods.sodium.client.gl.shader.*; +import me.jellysquid.mods.sodium.client.render.particle.shader.ParticleShaderBindingPoints; +import me.jellysquid.mods.sodium.client.render.particle.shader.ParticleShaderInterface; +import net.minecraft.util.Identifier; + +public class ShaderBillboardParticleRenderer { + protected GlProgram activeProgram; + + public ShaderBillboardParticleRenderer() { + this.activeProgram = createShader("particles/particle"); + } + + public GlProgram getActiveProgram() { + return activeProgram; + } + + private GlProgram createShader(String path) { + ShaderConstants constants = ShaderConstants.builder().build(); + + GlShader vertShader = ShaderLoader.loadShader(ShaderType.VERTEX, + new Identifier("sodium", path + ".vsh"), constants); + + GlShader fragShader = ShaderLoader.loadShader(ShaderType.FRAGMENT, + new Identifier("sodium", path + ".fsh"), constants); + + try { + return GlProgram.builder(new Identifier("sodium", "billboard_particle_shader")) + .attachShader(vertShader) + .attachShader(fragShader) + .bindFragmentData("out_FragColor", ParticleShaderBindingPoints.FRAG_COLOR) + .link(ParticleShaderInterface::new); + } finally { + vertShader.delete(); + fragShader.delete(); + } + } + + public void begin() { + this.activeProgram.bind(); + } + + public void setupState() { + this.activeProgram.getInterface().setupState(); + } + + public void end() { + this.activeProgram.unbind(); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/particle/cache/ParticleTextureCache.java b/src/main/java/me/jellysquid/mods/sodium/client/render/particle/cache/ParticleTextureCache.java new file mode 100644 index 0000000000..ec38d1e24d --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/particle/cache/ParticleTextureCache.java @@ -0,0 +1,80 @@ +package me.jellysquid.mods.sodium.client.render.particle.cache; + +import it.unimi.dsi.fastutil.ints.IntArrayFIFOQueue; +import it.unimi.dsi.fastutil.ints.IntList; +import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import net.caffeinemc.mods.sodium.api.util.RawUVs; + +import java.util.*; + +public class ParticleTextureCache { + private static final int DEFAULT_CAPACITY = 64; + + private RawUVs[] rawUVs; + private final Long2IntOpenHashMap uvToIndex = new Long2IntOpenHashMap(); + private final TextureUsageMonitor usageMonitor = new TextureUsageMonitor(); + + private final IntArrayFIFOQueue freeIndices = new IntArrayFIFOQueue(); + private int topIndex = 0; + + public ParticleTextureCache() { + this.rawUVs = new RawUVs[DEFAULT_CAPACITY]; + } + + public int getTopIndex() { + return topIndex; + } + + public int getUvIndex(RawUVs uvs) { + long uvKey = uvs.key(); + + int use = uvToIndex.computeIfAbsent(uvKey, key -> { + int index = freeIndices.isEmpty() ? topIndex++ : freeIndices.dequeueInt(); + + ensureCapacity(index); + rawUVs[index] = uvs; + return index; + }); + + usageMonitor.markUsed(use); + return use; + } + + public void markTextureAsUsed(int index) { + usageMonitor.markUsed(index); + } + + /** + * @return The array of RawUVs that should be uploaded + */ + public RawUVs[] update() { + IntList toRemove = this.usageMonitor.update(); + for (int i = 0; i < toRemove.size(); i++) { + int index = toRemove.getInt(i); + + RawUVs uvs = rawUVs[index]; + rawUVs[index] = null; + + this.freeIndices.enqueue(index); + uvToIndex.remove(uvs.key()); + } + return this.rawUVs; + } + + private void ensureCapacity(int high) { + if (high >= this.rawUVs.length) { + reallocRawUVs(high); + } + } + + private void reallocRawUVs(int high) { + int newCapacity = this.rawUVs.length; + while (high >= newCapacity) { + newCapacity += (newCapacity >> 1); + } + RawUVs[] newArray = new RawUVs[newCapacity]; + System.arraycopy(rawUVs, 0, newArray, 0, rawUVs.length); + this.rawUVs = newArray; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/particle/cache/TextureUsageMonitor.java b/src/main/java/me/jellysquid/mods/sodium/client/render/particle/cache/TextureUsageMonitor.java new file mode 100644 index 0000000000..9f54f4ec2a --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/particle/cache/TextureUsageMonitor.java @@ -0,0 +1,64 @@ +package me.jellysquid.mods.sodium.client.render.particle.cache; + +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntCollection; +import it.unimi.dsi.fastutil.ints.IntList; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; + +import java.util.Arrays; + +public class TextureUsageMonitor { + private static final int DEFAULT_INITIAL_CAPACITY = 64; + + private static final byte REMOVE_AFTER = 4; + + private byte[] lastUsed; + + public TextureUsageMonitor(int initialCapacity) { + this.lastUsed = new byte[initialCapacity]; + Arrays.fill(lastUsed, (byte) -1); + } + + public TextureUsageMonitor() { + this(DEFAULT_INITIAL_CAPACITY); + } + + public void markUsed(int index) { + ensureCapacity(index); + lastUsed[index] = 0; + } + + /** + * @return a List of the index of the UVs that should be removed + */ + public IntList update() { + IntList toRemove = new IntArrayList(); + for (int i = 0; i < lastUsed.length; ++i) { + byte v = lastUsed[i]; + if (v < 0) continue; + + byte framesUnused = ++lastUsed[i]; + if (framesUnused > REMOVE_AFTER) { + lastUsed[i] = -1; + toRemove.add(i); + } + } + + return toRemove; + } + + private void ensureCapacity(int high) { + if (high >= this.lastUsed.length) reallocLastUsed(high); + } + + private void reallocLastUsed(int high) { + int newCapacity = this.lastUsed.length; + while (high >= newCapacity) { + newCapacity += (newCapacity >> 1); + } + byte[] newArray = new byte[newCapacity]; + System.arraycopy(lastUsed, 0, newArray, 0, lastUsed.length); + Arrays.fill(newArray, lastUsed.length, newCapacity, (byte) -1); + this.lastUsed = newArray; + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/particle/shader/BillboardParticleData.java b/src/main/java/me/jellysquid/mods/sodium/client/render/particle/shader/BillboardParticleData.java new file mode 100644 index 0000000000..b577ca1f12 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/particle/shader/BillboardParticleData.java @@ -0,0 +1,28 @@ +package me.jellysquid.mods.sodium.client.render.particle.shader; + +import net.caffeinemc.mods.sodium.api.vertex.attributes.common.ColorAttribute; +import net.caffeinemc.mods.sodium.api.vertex.attributes.common.LightAttribute; +import net.caffeinemc.mods.sodium.api.vertex.attributes.common.PositionAttribute; +import net.caffeinemc.mods.sodium.api.vertex.attributes.common.TextureAttribute; +import org.lwjgl.system.MemoryUtil; + +public class BillboardParticleData { + public static final int POSITION_OFFSET = 0; + public static final int SIZE_OFFSET = 12; + public static final int COLOR_OFFSET = 16; + public static final int LIGHT_UV_OFFSET = 20; + public static final int ANGLE_OFFSET = 24; + public static final int TEXTURE_INDEX_OFFSET = 28; + public static final int STRIDE = 32; + + public static void put(long ptr, float x, float y, float z, int color, + int light, float size, float angle, int textureIndex + ) { + PositionAttribute.put(ptr + POSITION_OFFSET, x, y, z); + MemoryUtil.memPutFloat(ptr + SIZE_OFFSET, size); + ColorAttribute.set(ptr + COLOR_OFFSET, color); + LightAttribute.set(ptr + LIGHT_UV_OFFSET, light); + MemoryUtil.memPutFloat(ptr + ANGLE_OFFSET, angle); + MemoryUtil.memPutInt(ptr + TEXTURE_INDEX_OFFSET, textureIndex); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/particle/shader/ParticleShaderBindingPoints.java b/src/main/java/me/jellysquid/mods/sodium/client/render/particle/shader/ParticleShaderBindingPoints.java new file mode 100644 index 0000000000..1d21f3beb3 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/particle/shader/ParticleShaderBindingPoints.java @@ -0,0 +1,5 @@ +package me.jellysquid.mods.sodium.client.render.particle.shader; + +public class ParticleShaderBindingPoints { + public static final int FRAG_COLOR = 0; +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/particle/shader/ParticleShaderInterface.java b/src/main/java/me/jellysquid/mods/sodium/client/render/particle/shader/ParticleShaderInterface.java new file mode 100644 index 0000000000..d2c8cd9747 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/particle/shader/ParticleShaderInterface.java @@ -0,0 +1,81 @@ +package me.jellysquid.mods.sodium.client.render.particle.shader; + +import com.mojang.blaze3d.platform.GlStateManager; +import com.mojang.blaze3d.systems.RenderSystem; +import me.jellysquid.mods.sodium.client.gl.shader.uniform.GlUniformInt; +import me.jellysquid.mods.sodium.client.gl.shader.uniform.GlUniformMatrix4f; +import me.jellysquid.mods.sodium.client.render.chunk.shader.ShaderBindingContext; +import me.jellysquid.mods.sodium.client.util.TextureUtil; +import org.joml.Matrix4fc; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL31; +import org.lwjgl.opengl.GL32C; + +public class ParticleShaderInterface { + private final GlUniformInt uniformParticleTexture; + private final GlUniformInt uniformLightTexture; + private final GlUniformMatrix4f uniformModelViewMatrix; + private final GlUniformMatrix4f uniformProjectionMatrix; + private final GlUniformInt uniformParticleData; + private final GlUniformInt uniformTextureCache; + + public ParticleShaderInterface(ShaderBindingContext context) { + this.uniformParticleTexture = context.bindUniform("u_ParticleTex", GlUniformInt::new); + this.uniformLightTexture = context.bindUniform("u_LightTex", GlUniformInt::new); + this.uniformModelViewMatrix = context.bindUniform("u_ModelViewMatrix", GlUniformMatrix4f::new); + this.uniformProjectionMatrix = context.bindUniform("u_ProjectionMatrix", GlUniformMatrix4f::new); + this.uniformParticleData = context.bindUniform("u_ParticleData", GlUniformInt::new); + this.uniformTextureCache = context.bindUniform("u_TextureCache", GlUniformInt::new); + } + + public void setProjectionMatrix(Matrix4fc matrix) { + this.uniformProjectionMatrix.set(matrix); + } + + public void setModelViewMatrix(Matrix4fc matrix) { + this.uniformModelViewMatrix.set(matrix); + } + + public void setupState() { + // "BlockTexture" should represent the particle textures if bound correctly + this.bindParticleTexture(ParticleShaderTextureSlot.TEXTURE, TextureUtil.getBlockTextureId()); + this.bindLightTexture(ParticleShaderTextureSlot.LIGHT, TextureUtil.getLightTextureId()); + this.bindParticleData(ParticleShaderTextureSlot.PARTICLE_DATA, RenderSystem.getShaderTexture(3)); + this.bindTextureCache(ParticleShaderTextureSlot.TEXTURE_CACHE, RenderSystem.getShaderTexture(4)); + } + + private void bindParticleTexture(ParticleShaderTextureSlot slot, int textureId) { + GlStateManager._activeTexture(GL32C.GL_TEXTURE0 + slot.ordinal()); + GlStateManager._bindTexture(textureId); + + uniformParticleTexture.setInt(slot.ordinal()); + } + + private void bindLightTexture(ParticleShaderTextureSlot slot, int textureId) { + GlStateManager._activeTexture(GL32C.GL_TEXTURE0 + slot.ordinal()); + GlStateManager._bindTexture(textureId); + + uniformLightTexture.setInt(slot.ordinal()); + } + + private void bindParticleData(ParticleShaderTextureSlot slot, int textureId) { + GlStateManager._activeTexture(GL32C.GL_TEXTURE0 + slot.ordinal()); + GL11.glBindTexture(GL31.GL_TEXTURE_BUFFER, textureId); + + uniformParticleData.setInt(slot.ordinal()); + } + + private void bindTextureCache(ParticleShaderTextureSlot slot, int textureId) { + GlStateManager._activeTexture(GL32C.GL_TEXTURE0 + slot.ordinal()); + GL11.glBindTexture(GL31.GL_TEXTURE_BUFFER, textureId); + + uniformTextureCache.setInt(slot.ordinal()); + } + + private enum ParticleShaderTextureSlot { + TEXTURE, + LIGHT, + PARTICLE_DATA, + TEXTURE_CACHE, + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/util/collections/EvictingPool.java b/src/main/java/me/jellysquid/mods/sodium/client/util/collections/EvictingPool.java new file mode 100644 index 0000000000..4f60068638 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/util/collections/EvictingPool.java @@ -0,0 +1,205 @@ +package me.jellysquid.mods.sodium.client.util.collections; + +import it.unimi.dsi.fastutil.ints.IntArrayFIFOQueue; +import it.unimi.dsi.fastutil.ints.IntIntMutablePair; +import org.jetbrains.annotations.NotNull; + +import java.util.AbstractCollection; +import java.util.Arrays; +import java.util.Iterator; + +/** + * Functions similarly to an EvictingQueue, except existing elements + * stay in the same place in memory until they are removed or replaced + * in favor of a new element. + * @param + */ +public class EvictingPool extends AbstractCollection { + E[] pool; + Link[] links; + int oldestIdx; + int newestIdx; + + IntArrayFIFOQueue free; + int top; + + int size; + + public EvictingPool(int size) { + this.pool = (E[]) new Object[size]; + this.links = new Link[size]; + Arrays.asList(this.links).replaceAll(i -> new Link(-1, -1)); + + this.free = new IntArrayFIFOQueue(); + this.top = 0; + this.size = size; + + this.oldestIdx = -1; + this.newestIdx = -1; + } + + public boolean add(E item) { + if (free.isEmpty()) { + if (this.top < size) { + pool[this.top] = item; + this.newest(this.top); + + ++this.top; + } else { + pool[this.oldestIdx] = item; + int nextOldest = links[this.oldestIdx].next(); + links[nextOldest].prev(-1); + + this.newest(this.oldestIdx); + this.oldestIdx = nextOldest; + } + } else { + int freeIdx = free.dequeueInt(); + pool[freeIdx] = item; + this.newest(freeIdx); + } + + return true; + } + + public void remove(int index) { + free.enqueue(index); + Link link = this.links[index]; + + System.out.println(link); + + if (index == oldestIdx) { + links[link.next()].prev(-1); + this.oldestIdx = link.next(); + } else if (index == newestIdx) { + links[link.prev()].next(-1); + this.newestIdx = link.prev(); + } else { + links[link.prev()].next(link.next()); + links[link.next()].prev(link.prev()); + } + + pool[index] = null; + } + + @Override + public int size() { + return top - free.size(); + } + + public @NotNull Iterator iterator() { + if (this.saturation() < Itr.MAGIC_RATIO) { + return new LinkItr(); + } else { + return new IncrementItr(); + } + } + + private double saturation() { + return (double) top / free.size(); + } + + private void newest(int idx) { + int prevNewest = this.newestIdx; + this.newestIdx = idx; + + if (prevNewest >= 0) links[prevNewest].next(newestIdx); + links[newestIdx] = new Link(prevNewest, -1); + } + + /** + * Added to prevent confusion on left/right + */ + private static class Link extends IntIntMutablePair { + public Link(int prev, int next) { + super(prev, next); + } + + public int next() { + return this.right; + } + + public void next(int next) { + this.right = next; + } + + public int prev() { + return this.left; + } + + public void prev(int prev) { + this.left = prev; + } + } + + private abstract class Itr implements Iterator { + static final double MAGIC_RATIO = 0.50; + int cursor = -1; + + @Override + public void remove() { + EvictingPool.this.remove(cursor); + } + } + + private class LinkItr extends Itr { + boolean beganIterating; + Link currentLink; + + LinkItr() { + this.cursor = -1; + this.currentLink = null; + } + + @Override + public boolean hasNext() { + return this.currentLink.next() != -1; + } + + @Override + public E next() { + if (!beganIterating) { + this.cursor = EvictingPool.this.newestIdx; + beganIterating = true; + } else { + this.cursor = this.currentLink.next(); + } + + this.currentLink = EvictingPool.this.links[this.cursor]; + return EvictingPool.this.pool[this.cursor]; + } + } + + private class IncrementItr extends Itr { + E next = null; + int nextCursor = -1; + + IncrementItr() { + this.cursor = -1; + this.next = this.getNext(); + } + + @Override + public boolean hasNext() { + return next != null; + } + + @Override + public E next() { + E ret = this.next; + this.cursor = this.nextCursor; + this.next = this.getNext(); + return ret; + } + + private E getNext() { + E ret = null; + while (ret == null && this.nextCursor + 1 < EvictingPool.this.top) { + ++this.nextCursor; + ret = EvictingPool.this.pool[this.nextCursor]; + } + + return ret; + } + } +} \ No newline at end of file diff --git a/src/main/java/me/jellysquid/mods/sodium/mixin/features/render/particle/BillboardParticleMixin.java b/src/main/java/me/jellysquid/mods/sodium/mixin/features/render/particle/BillboardParticleMixin.java index 9e3d4bfff1..122c696b01 100644 --- a/src/main/java/me/jellysquid/mods/sodium/mixin/features/render/particle/BillboardParticleMixin.java +++ b/src/main/java/me/jellysquid/mods/sodium/mixin/features/render/particle/BillboardParticleMixin.java @@ -1,8 +1,14 @@ package me.jellysquid.mods.sodium.mixin.features.render.particle; -import net.caffeinemc.mods.sodium.api.vertex.format.common.ParticleVertex; -import net.caffeinemc.mods.sodium.api.vertex.buffer.VertexBufferWriter; +import me.jellysquid.mods.sodium.client.gl.arena.staging.StagingBufferBuilder; +import me.jellysquid.mods.sodium.client.render.particle.BillboardExtended; +import me.jellysquid.mods.sodium.client.render.particle.shader.BillboardParticleData; +import me.jellysquid.mods.sodium.client.render.particle.cache.ParticleTextureCache; +import net.caffeinemc.mods.sodium.api.buffer.UnmanagedBufferBuilder; import net.caffeinemc.mods.sodium.api.util.ColorABGR; +import net.caffeinemc.mods.sodium.api.util.RawUVs; +import net.caffeinemc.mods.sodium.api.vertex.buffer.VertexBufferWriter; +import net.caffeinemc.mods.sodium.api.vertex.format.common.ParticleVertex; import net.minecraft.client.particle.BillboardParticle; import net.minecraft.client.particle.Particle; import net.minecraft.client.render.Camera; @@ -18,7 +24,13 @@ import org.spongepowered.asm.mixin.Unique; @Mixin(BillboardParticle.class) -public abstract class BillboardParticleMixin extends Particle { +public abstract class BillboardParticleMixin extends Particle implements BillboardExtended { + @Unique + private long prevUvKey = 0x7fc000007fc00000L; + + @Unique + private int prevTextureIndex = -1; + @Shadow public abstract float getSize(float tickDelta); @@ -38,6 +50,45 @@ protected BillboardParticleMixin(ClientWorld world, double x, double y, double z super(world, x, y, z); } + @Override + public void sodium$buildParticleData( + StagingBufferBuilder builder, + ParticleTextureCache registry, + Camera camera, float tickDelta + ) { + Vec3d vec3d = camera.getPos(); + RawUVs uvs = new RawUVs(getMinU(), getMinV(), getMaxU(), getMaxV()); + + int textureIndex; + long uvKey = uvs.key(); + + if (prevTextureIndex == -1 || uvKey != prevUvKey) { + textureIndex = registry.getUvIndex(uvs); + prevTextureIndex = textureIndex; + prevUvKey = uvKey; + } else { + textureIndex = prevTextureIndex; + registry.markTextureAsUsed(textureIndex); + } + + float x = (float) (MathHelper.lerp(tickDelta, this.prevPosX, this.x) - vec3d.getX()); + float y = (float) (MathHelper.lerp(tickDelta, this.prevPosY, this.y) - vec3d.getY()); + float z = (float) (MathHelper.lerp(tickDelta, this.prevPosZ, this.z) - vec3d.getZ()); + + float size = this.getSize(tickDelta); + int light = this.getBrightness(tickDelta); + + int color = ColorABGR.pack(this.red , this.green, this.blue, this.alpha); + + float angle = MathHelper.lerp(tickDelta, this.prevAngle, this.angle); + + try (MemoryStack stack = MemoryStack.stackPush()) { + long ptr = stack.nmalloc(BillboardParticleData.STRIDE); + BillboardParticleData.put(ptr, x, y, z, color, light, size, angle, textureIndex); + builder.push(stack, ptr, BillboardParticleData.STRIDE); + } + } + /** * @reason Optimize function * @author JellySquid diff --git a/src/main/java/me/jellysquid/mods/sodium/mixin/features/render/particle/ParticleManagerMixin.java b/src/main/java/me/jellysquid/mods/sodium/mixin/features/render/particle/ParticleManagerMixin.java new file mode 100644 index 0000000000..2561899aec --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/mixin/features/render/particle/ParticleManagerMixin.java @@ -0,0 +1,337 @@ +package me.jellysquid.mods.sodium.mixin.features.render.particle; + +import com.google.common.collect.EvictingQueue; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.mojang.blaze3d.platform.GlStateManager; +import com.mojang.blaze3d.systems.RenderSystem; +import it.unimi.dsi.fastutil.objects.Object2BooleanMap; +import it.unimi.dsi.fastutil.objects.Object2BooleanOpenHashMap; +import me.jellysquid.mods.sodium.client.gl.arena.staging.StagingBufferBuilder; +import me.jellysquid.mods.sodium.client.gl.attribute.GlVertexAttributeFormat; +import me.jellysquid.mods.sodium.client.gl.device.CommandList; +import me.jellysquid.mods.sodium.client.gl.device.RenderDevice; +import me.jellysquid.mods.sodium.client.render.particle.*; +import me.jellysquid.mods.sodium.client.render.particle.cache.ParticleTextureCache; +import me.jellysquid.mods.sodium.client.render.particle.shader.ParticleShaderInterface; +import net.caffeinemc.mods.sodium.api.buffer.UnmanagedBufferBuilder; +import net.caffeinemc.mods.sodium.api.util.RawUVs; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.client.particle.*; +import net.minecraft.client.render.Camera; +import net.minecraft.client.render.LightmapTextureManager; +import net.minecraft.client.render.VertexConsumer; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.texture.SpriteAtlasTexture; +import net.minecraft.client.texture.TextureManager; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.util.Identifier; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL20C; +import org.lwjgl.system.MemoryStack; +import org.spongepowered.asm.mixin.*; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +import java.util.*; + +@Mixin(ParticleManager.class) +public abstract class ParticleManagerMixin { + /** + * The set of particle classes that can use the GPU fast path.
+ * If the class overrides the build geometry method, + * it should have a mixin where {@link BillboardExtended#sodium$buildParticleData} + * is overridden to produce the correct behavior. + * See the specialcases package for examples. + */ + @Unique + private static final Set> FAST_PATH_PARTICLES = Set.of( + BillboardParticle.class, + SpriteBillboardParticle.class, + + DustColorTransitionParticle.class, + FireworksSparkParticle.Explosion.class, + FireworksSparkParticle.Flash.class + ); + + @Shadow + protected ClientWorld world; + + @Shadow + @Final + private static List PARTICLE_TEXTURE_SHEETS; + + @Shadow + @Final + private Queue newEmitterParticles; + + @Shadow + @Final + private Queue newParticles; + + @Shadow + @Final + private Map> particles; + + @Unique + private final Map> billboardParticles = Maps.newIdentityHashMap(); + + @Unique + private final ShaderBillboardParticleRenderer particleRenderer = new ShaderBillboardParticleRenderer(); + + @Unique + private final ParticleTextureCache particleTexCache = new ParticleTextureCache(); + + @Unique + private static final Object2BooleanMap> classOverridesBuild = new Object2BooleanOpenHashMap<>(); + + @Unique + private ParticleRenderView renderView; + + @Unique + private static final String BUILD_GEOMETRY_METHOD = FabricLoader.getInstance().getMappingResolver().mapMethodName( + "intermediary", + "net.minecraft.class_703", + "method_3074", + "(Lnet/minecraft/class_4588;Lnet/minecraft/class_4184;F)V" + ); + + @Unique + private int glVertexArray; + + @Unique + private StagingBufferBuilder dataBufferBuilder = null; + + @Unique + private UnmanagedBufferBuilder cacheBufferBuilder; + + @Unique + private ParticleBuffers buffers = null; + + @Unique + private Identifier prevTexture = null; + + @Inject(method = "", at = @At("RETURN")) + private void postInit(ClientWorld world, TextureManager textureManager, CallbackInfo ci) { + this.glVertexArray = GlStateManager._glGenVertexArrays(); + // 2 * 16384 * 32 + this.cacheBufferBuilder = new UnmanagedBufferBuilder(1); + this.renderView = new ParticleRenderView(world); + } + + @Shadow + protected abstract void tickParticles(Collection particles); + + /** + * @author BeljihnWahfl + * @reason Could not feasibly inject all needed functionality + */ + @Overwrite + public void tick() { + this.particles.forEach((sheet, queue) -> { + this.world.getProfiler().push(sheet.toString()); + this.tickParticles(queue); + this.world.getProfiler().pop(); + }); + + this.billboardParticles.forEach((sheet, queue) -> { + this.world.getProfiler().push(sheet.toString()); + // This is safe because tickParticles never adds to the collection. + this.tickParticles((Collection) queue); + this.world.getProfiler().pop(); + }); + + if (!this.newEmitterParticles.isEmpty()) { + List list = Lists.newArrayList(); + + for(EmitterParticle emitterParticle : this.newEmitterParticles) { + emitterParticle.tick(); + if (!emitterParticle.isAlive()) { + list.add(emitterParticle); + } + } + + this.newEmitterParticles.removeAll(list); + } + + Particle particle; + if (!this.newParticles.isEmpty()) { + while((particle = this.newParticles.poll()) != null) { + if (particle instanceof BillboardParticle bParticle && !classOverridesBuild.computeIfAbsent( + bParticle.getClass(), + this::testClassOverrides + )) { + this.billboardParticles + .computeIfAbsent(particle.getType(), sheet -> EvictingQueue.create(16384)) + .add((BillboardParticle) particle); + } else { + this.particles + .computeIfAbsent(particle.getType(), sheet -> EvictingQueue.create(16384)) + .add(particle); + } + } + } + } + + @Unique + private boolean testClassOverrides(Class particleClass) { + try { + return !FAST_PATH_PARTICLES.contains(particleClass.getMethod( + BUILD_GEOMETRY_METHOD, + VertexConsumer.class, + Camera.class, + float.class + ).getDeclaringClass()); + } catch (NoSuchMethodException e) { + return true; + } + } + + @Inject(method = "clearParticles", at = @At("TAIL")) + private void clearParticles(CallbackInfo ci) { + this.billboardParticles.clear(); + } + + @Inject(method = "renderParticles", at = @At("HEAD")) + private void preRenderParticles(MatrixStack matrices, + VertexConsumerProvider.Immediate vertexConsumers, + LightmapTextureManager lightmapTextureManager, + Camera camera, + float tickDelta, + CallbackInfo ci) { + this.renderView.resetCache(); + } + + @Inject( + method = "renderParticles", + at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/systems/RenderSystem;applyModelViewMatrix()V", ordinal = 0, shift = At.Shift.AFTER), + locals = LocalCapture.CAPTURE_FAILHARD + ) + public void renderParticles( + MatrixStack matrices, VertexConsumerProvider.Immediate vertexConsumers, + LightmapTextureManager lightmapTextureManager, Camera camera, float tickDelta, + CallbackInfo ci, MatrixStack matrixStack + ) { + RenderDevice.enterManagedCode(); + try (CommandList commands = RenderDevice.INSTANCE.createCommandList()) { + if (this.buffers == null) { + this.buffers = new ParticleBuffers(commands); + this.dataBufferBuilder = new StagingBufferBuilder(commands, 1024 * 1024 * 16); + } + + particleRenderer.begin(); + ParticleShaderInterface shader = this.particleRenderer.getActiveProgram().getInterface(); + shader.setProjectionMatrix(RenderSystem.getProjectionMatrix()); + shader.setModelViewMatrix(RenderSystem.getModelViewMatrix()); + + for (ParticleTextureSheet particleTextureSheet : PARTICLE_TEXTURE_SHEETS) { + Queue iterable = this.billboardParticles.get(particleTextureSheet); + if (iterable != null && !iterable.isEmpty()) { + int numParticles = iterable.size(); + bindParticleTextureSheet(particleTextureSheet); + this.buffers.bind(); + particleRenderer.setupState(); + + for (BillboardParticle particle : iterable) { + ((BillboardExtended) particle).sodium$buildParticleData( + dataBufferBuilder, + particleTexCache, + camera, tickDelta + ); + } + + drawParticleTextureSheet(commands, particleTextureSheet, numParticles); + } + } + } finally { + prevTexture = null; + dataBufferBuilder.flipFrame(); + particleRenderer.end(); + RenderDevice.exitManagedCode(); + } + } + + @Unique + @SuppressWarnings("deprecation") + private void bindParticleTextureSheet(ParticleTextureSheet sheet) { + RenderSystem.depthMask(true); + Identifier texture = null; + if (sheet == ParticleTextureSheet.PARTICLE_SHEET_LIT || sheet == ParticleTextureSheet.PARTICLE_SHEET_OPAQUE) { + RenderSystem.disableBlend(); + texture = SpriteAtlasTexture.PARTICLE_ATLAS_TEXTURE; + } else if (sheet == ParticleTextureSheet.PARTICLE_SHEET_TRANSLUCENT) { + RenderSystem.enableBlend(); + texture = SpriteAtlasTexture.PARTICLE_ATLAS_TEXTURE; + } else if (sheet == ParticleTextureSheet.TERRAIN_SHEET) { + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + texture = SpriteAtlasTexture.BLOCK_ATLAS_TEXTURE; + } else if (sheet == ParticleTextureSheet.CUSTOM) { + RenderSystem.depthMask(true); + RenderSystem.disableBlend(); + } + + if (texture != null && !texture.equals(prevTexture)) { + RenderSystem.setShaderTexture(0, texture); + this.prevTexture = texture; + } + } + + @Unique + private void drawParticleTextureSheet(CommandList commands, ParticleTextureSheet sheet, int numParticles) { + if (sheet == ParticleTextureSheet.TERRAIN_SHEET || sheet == ParticleTextureSheet.PARTICLE_SHEET_LIT || sheet == ParticleTextureSheet.PARTICLE_SHEET_OPAQUE || sheet == ParticleTextureSheet.PARTICLE_SHEET_TRANSLUCENT) { + uploadParticleBuffer(commands); + bindDummyVao(); + GL11.glDrawArrays(GL11.GL_TRIANGLES, 0, numParticles * 6); + } + } + + @Unique + private void bindDummyVao() { + GlStateManager._glBindVertexArray(this.glVertexArray); + GL20C.glVertexAttribPointer(0, 1, GlVertexAttributeFormat.UNSIGNED_BYTE.typeId(), false, 1, 0); + GL20C.glEnableVertexAttribArray(0); + } + + @Unique + private void uploadParticleBuffer(CommandList commands) { + RawUVs[] toUpload = this.particleTexCache.update(); + int maxUploadIndex = this.particleTexCache.getTopIndex(); + + try (MemoryStack stack = MemoryStack.stackPush()) { + int size = RawUVs.STRIDE * maxUploadIndex; + + long buffer = stack.nmalloc(size); + long ptr = buffer; + for (int i = 0; i < maxUploadIndex; i++) { + RawUVs uvs = toUpload[i]; + if (uvs == null) { + RawUVs.putNull(ptr); + } else { + uvs.put(ptr); + } + ptr += RawUVs.STRIDE; + } + cacheBufferBuilder.push(stack, buffer, size); + } + + UnmanagedBufferBuilder.Built cache = cacheBufferBuilder.end(); + + this.buffers.uploadParticleData(commands, dataBufferBuilder, cache); + } + + @Inject(method = "setWorld", at = @At("RETURN")) + private void postSetWorld(ClientWorld world, CallbackInfo ci) { + this.renderView = new ParticleRenderView(world); + } + + @Inject(method = "addParticle(Lnet/minecraft/client/particle/Particle;)V", at = @At("HEAD")) + private void preAddParticle(Particle particle, CallbackInfo ci) { + if (particle instanceof ParticleExtended extension) { + extension.sodium$configure(this.renderView); + } + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/mixin/features/render/particle/ParticleMixin.java b/src/main/java/me/jellysquid/mods/sodium/mixin/features/render/particle/ParticleMixin.java new file mode 100644 index 0000000000..f76ccd0ee3 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/mixin/features/render/particle/ParticleMixin.java @@ -0,0 +1,39 @@ +package me.jellysquid.mods.sodium.mixin.features.render.particle; + +import me.jellysquid.mods.sodium.client.render.particle.ParticleExtended; +import me.jellysquid.mods.sodium.client.render.particle.ParticleRenderView; +import net.minecraft.client.particle.Particle; +import net.minecraft.util.math.MathHelper; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(Particle.class) +public class ParticleMixin implements ParticleExtended { + @Shadow + protected double x; + @Shadow + protected double y; + @Shadow + protected double z; + + @Unique + private ParticleRenderView renderView; + + @Override + public void sodium$configure(ParticleRenderView renderView) { + this.renderView = renderView; + } + + /** + * @author JellySquid + * @reason Use render cache + */ + @Overwrite + public int getBrightness(float tickDelta) { + return this.renderView.getBrightness(MathHelper.floor(this.x), + MathHelper.floor(this.y), + MathHelper.floor(this.z)); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/mixin/features/render/particle/specialcases/DustColorTransitionParticleMixin.java b/src/main/java/me/jellysquid/mods/sodium/mixin/features/render/particle/specialcases/DustColorTransitionParticleMixin.java new file mode 100644 index 0000000000..204046209d --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/mixin/features/render/particle/specialcases/DustColorTransitionParticleMixin.java @@ -0,0 +1,27 @@ +package me.jellysquid.mods.sodium.mixin.features.render.particle.specialcases; + +import me.jellysquid.mods.sodium.client.gl.arena.staging.StagingBufferBuilder; +import me.jellysquid.mods.sodium.client.render.particle.cache.ParticleTextureCache; +import me.jellysquid.mods.sodium.mixin.features.render.particle.BillboardParticleMixin; +import net.caffeinemc.mods.sodium.api.buffer.UnmanagedBufferBuilder; +import net.minecraft.client.particle.DustColorTransitionParticle; +import net.minecraft.client.render.Camera; +import net.minecraft.client.world.ClientWorld; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(DustColorTransitionParticle.class) +public abstract class DustColorTransitionParticleMixin extends BillboardParticleMixin { + @Shadow + protected abstract void updateColor(float tickDelta); + + protected DustColorTransitionParticleMixin(ClientWorld world, double x, double y, double z) { + super(world, x, y, z); + } + + @Override + public void sodium$buildParticleData(StagingBufferBuilder builder, ParticleTextureCache registry, Camera camera, float tickDelta) { + this.updateColor(tickDelta); + super.sodium$buildParticleData(builder, registry, camera, tickDelta); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/mixin/features/render/particle/specialcases/FireworksSparkParticleMixin.java b/src/main/java/me/jellysquid/mods/sodium/mixin/features/render/particle/specialcases/FireworksSparkParticleMixin.java new file mode 100644 index 0000000000..c83189bca6 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/mixin/features/render/particle/specialcases/FireworksSparkParticleMixin.java @@ -0,0 +1,28 @@ +package me.jellysquid.mods.sodium.mixin.features.render.particle.specialcases; + +import me.jellysquid.mods.sodium.client.gl.arena.staging.StagingBufferBuilder; +import me.jellysquid.mods.sodium.client.render.particle.cache.ParticleTextureCache; +import me.jellysquid.mods.sodium.mixin.features.render.particle.BillboardParticleMixin; +import net.caffeinemc.mods.sodium.api.buffer.UnmanagedBufferBuilder; +import net.minecraft.client.particle.FireworksSparkParticle; +import net.minecraft.client.render.Camera; +import net.minecraft.client.world.ClientWorld; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(FireworksSparkParticle.Explosion.class) +public abstract class FireworksSparkParticleMixin extends BillboardParticleMixin { + @Shadow + private boolean flicker; + + protected FireworksSparkParticleMixin(ClientWorld world, double x, double y, double z) { + super(world, x, y, z); + } + + @Override + public void sodium$buildParticleData(StagingBufferBuilder builder, ParticleTextureCache registry, Camera camera, float tickDelta) { + if (!this.flicker || this.age < this.maxAge / 3 || (this.age + this.maxAge) / 3 % 2 == 0) { + super.sodium$buildParticleData(builder, registry, camera, tickDelta); + } + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/mixin/features/render/particle/specialcases/FlashParticleMixin.java b/src/main/java/me/jellysquid/mods/sodium/mixin/features/render/particle/specialcases/FlashParticleMixin.java new file mode 100644 index 0000000000..78c2c41f14 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/mixin/features/render/particle/specialcases/FlashParticleMixin.java @@ -0,0 +1,23 @@ +package me.jellysquid.mods.sodium.mixin.features.render.particle.specialcases; + +import me.jellysquid.mods.sodium.client.gl.arena.staging.StagingBufferBuilder; +import me.jellysquid.mods.sodium.client.render.particle.cache.ParticleTextureCache; +import me.jellysquid.mods.sodium.mixin.features.render.particle.BillboardParticleMixin; +import net.caffeinemc.mods.sodium.api.buffer.UnmanagedBufferBuilder; +import net.minecraft.client.particle.FireworksSparkParticle; +import net.minecraft.client.render.Camera; +import net.minecraft.client.world.ClientWorld; +import org.spongepowered.asm.mixin.Mixin; + +@Mixin(FireworksSparkParticle.Flash.class) +public abstract class FlashParticleMixin extends BillboardParticleMixin { + protected FlashParticleMixin(ClientWorld world, double x, double y, double z) { + super(world, x, y, z); + } + + @Override + public void sodium$buildParticleData(StagingBufferBuilder builder, ParticleTextureCache registry, Camera camera, float tickDelta) { + this.setAlpha(0.6F - ((float)this.age + tickDelta - 1.0F) * 0.25F * 0.5F); + super.sodium$buildParticleData(builder, registry, camera, tickDelta); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/mixin/features/textures/animations/tracking/SpriteBillboardParticleMixin.java b/src/main/java/me/jellysquid/mods/sodium/mixin/features/textures/animations/tracking/SpriteBillboardParticleMixin.java index 3c0c1ffa21..b69bd73177 100644 --- a/src/main/java/me/jellysquid/mods/sodium/mixin/features/textures/animations/tracking/SpriteBillboardParticleMixin.java +++ b/src/main/java/me/jellysquid/mods/sodium/mixin/features/textures/animations/tracking/SpriteBillboardParticleMixin.java @@ -1,7 +1,11 @@ package me.jellysquid.mods.sodium.mixin.features.textures.animations.tracking; +import me.jellysquid.mods.sodium.client.gl.arena.staging.StagingBufferBuilder; +import me.jellysquid.mods.sodium.client.render.particle.BillboardExtended; +import me.jellysquid.mods.sodium.client.render.particle.cache.ParticleTextureCache; import me.jellysquid.mods.sodium.client.render.texture.SpriteUtil; -import net.minecraft.client.particle.BillboardParticle; +import me.jellysquid.mods.sodium.mixin.features.render.particle.BillboardParticleMixin; +import net.caffeinemc.mods.sodium.api.buffer.UnmanagedBufferBuilder; import net.minecraft.client.particle.SpriteBillboardParticle; import net.minecraft.client.render.Camera; import net.minecraft.client.render.VertexConsumer; @@ -15,7 +19,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(SpriteBillboardParticle.class) -public abstract class SpriteBillboardParticleMixin extends BillboardParticle { +public abstract class SpriteBillboardParticleMixin extends BillboardParticleMixin implements BillboardExtended { @Shadow protected Sprite sprite; @@ -39,4 +43,13 @@ public void buildGeometry(VertexConsumer vertexConsumer, Camera camera, float ti super.buildGeometry(vertexConsumer, camera, tickDelta); } + + @Override + public void sodium$buildParticleData(StagingBufferBuilder builder, ParticleTextureCache registry, Camera camera, float tickDelta) { + if (this.shouldTickSprite) { + SpriteUtil.markSpriteActive(this.sprite); + } + + super.sodium$buildParticleData(builder, registry, camera, tickDelta); + } } \ No newline at end of file diff --git a/src/main/resources/assets/sodium/shaders/blocks/block_layer_opaque.vsh b/src/main/resources/assets/sodium/shaders/blocks/block_layer_opaque.vsh index db2513be15..ffd20b790b 100644 --- a/src/main/resources/assets/sodium/shaders/blocks/block_layer_opaque.vsh +++ b/src/main/resources/assets/sodium/shaders/blocks/block_layer_opaque.vsh @@ -2,7 +2,7 @@ #import #import -#import +#import #import out vec4 v_Color; diff --git a/src/main/resources/assets/sodium/shaders/include/chunk_matrices.glsl b/src/main/resources/assets/sodium/shaders/include/matrices.glsl similarity index 100% rename from src/main/resources/assets/sodium/shaders/include/chunk_matrices.glsl rename to src/main/resources/assets/sodium/shaders/include/matrices.glsl diff --git a/src/main/resources/assets/sodium/shaders/particles/particle.fsh b/src/main/resources/assets/sodium/shaders/particles/particle.fsh new file mode 100644 index 0000000000..540c96879e --- /dev/null +++ b/src/main/resources/assets/sodium/shaders/particles/particle.fsh @@ -0,0 +1,16 @@ +#version 330 core + +uniform sampler2D u_ParticleTex; + +in vec2 texCoord0; +in vec4 vertexColor; + +out vec4 out_FragColor; + +void main() { + vec4 color = texture(u_ParticleTex, texCoord0) * vertexColor; + if (color.a < 0.1) { + discard; + } + out_FragColor = color; +} diff --git a/src/main/resources/assets/sodium/shaders/particles/particle.vsh b/src/main/resources/assets/sodium/shaders/particles/particle.vsh new file mode 100644 index 0000000000..66a0c743a6 --- /dev/null +++ b/src/main/resources/assets/sodium/shaders/particles/particle.vsh @@ -0,0 +1,106 @@ +#version 330 + +#define COLOR_SCALE 1.0 / 255.0 +#define PARTICLE_STRIDE 8 +#define TEX_STRIDE 4 + +#import + +const vec2 OFFSETS[] = vec2[]( + vec2(-1, -1), + vec2(1, -1), + vec2(1, 1), + vec2(-1, 1) +); + +const int INDICES[] = int[]( + 0, 1, 2, + 0, 2, 3 +); + +uniform sampler2D u_LightTex; +uniform usamplerBuffer u_ParticleData; // R_32UI +uniform usamplerBuffer u_TextureCache; // R_32UI + +out vec2 texCoord0; +out vec4 vertexColor; + +// 8 x 4 = 32 bytes stride +vec3 position; +float size; +vec4 color; +ivec2 light; +float angle; +// int textureIndex; Not necessary to store here + +vec2 minTexUV; +vec2 maxTexUV; + +// Returns a collection of 4 bytes +// ptr is essentially multiplied by 4 since u_BufferTexture is R_32UI +uint readBuffer(int ptr) { + return texelFetch(u_ParticleData, ptr).x; +} + +float readBufferF(int ptr) { + return uintBitsToFloat(readBuffer(ptr)); +} + +vec3 readBufferPos(int ptr) { + return uintBitsToFloat(uvec3(readBuffer(ptr), readBuffer(ptr + 1), readBuffer(ptr + 2))); +} + +vec4 readBufferColor(int ptr) { + return vec4((uvec4(readBuffer(ptr)) >> uvec4(0, 8, 16, 24)) & uvec4(0xFFu)) * COLOR_SCALE; +} + +ivec2 readBufferLight(int ptr) { + return ivec2((uvec2(readBuffer(ptr)) >> uvec2(0, 16)) & uvec2(0xFFFFu)); +} + + +float readFloatTex(int ptr) { + return uintBitsToFloat(texelFetch(u_TextureCache, ptr).x); +} + +vec2 readBufferTex(int ptr) { + return vec2(readFloatTex(ptr), readFloatTex(ptr + 1)); +} + +void init() { + int base = PARTICLE_STRIDE * (gl_VertexID / 6); + + position = readBufferPos(base); + size = readBufferF(base + 3); + color = readBufferColor(base + 4); + light = readBufferLight(base + 5); + angle = readBufferF(base + 6); + int textureIndex = int(readBuffer(base + 7)); + + int texturePtr = textureIndex * TEX_STRIDE; + minTexUV = readBufferTex(texturePtr); + maxTexUV = readBufferTex(texturePtr + 2); +} + +void main() { + init(); + int vertexIndex = INDICES[gl_VertexID % 6]; + + vec2 texUVs[] = vec2[]( + maxTexUV, + vec2(maxTexUV.x, minTexUV.y), + minTexUV, + vec2(minTexUV.x, maxTexUV.y) + ); + + // The following code is an optimized variant of Minecraft's quaternion code by Zombye and Balint. + gl_Position = u_ModelViewMatrix * vec4(position, 1.0); + float s = sin(angle); + float c = cos(angle); + gl_Position.xy += mat2(c,-s, s, c) * OFFSETS[vertexIndex] * size; + gl_Position = u_ProjectionMatrix * gl_Position; + + texCoord0 = texUVs[vertexIndex]; + + vertexColor = color * texelFetch(u_LightTex, light / 16, 0); +} \ No newline at end of file diff --git a/src/main/resources/sodium.accesswidener b/src/main/resources/sodium.accesswidener index 6f69cf9242..c06ec504a2 100644 --- a/src/main/resources/sodium.accesswidener +++ b/src/main/resources/sodium.accesswidener @@ -12,6 +12,7 @@ accessible class net/minecraft/client/render/BackgroundRenderer$FogData accessible class net/minecraft/client/render/BackgroundRenderer$StatusEffectFogModifier accessible class net/minecraft/client/texture/TextureStitcher$Holder accessible class net/minecraft/world/biome/Biome$Weather +accessible class net/minecraft/client/particle/FireworksSparkParticle$Explosion accessible method net/minecraft/client/render/chunk/BlockBufferBuilderPool (Ljava/util/List;)V accessible method net/minecraft/client/util/math/MatrixStack$Entry (Lorg/joml/Matrix4f;Lorg/joml/Matrix3f;)V diff --git a/src/main/resources/sodium.mixins.json b/src/main/resources/sodium.mixins.json index 8f0769414c..c67a4e7667 100644 --- a/src/main/resources/sodium.mixins.json +++ b/src/main/resources/sodium.mixins.json @@ -60,6 +60,8 @@ "features.render.model.block.BlockModelRendererMixin", "features.render.model.item.ItemRendererMixin", "features.render.particle.BillboardParticleMixin", + "features.render.particle.ParticleManagerMixin", + "features.render.particle.ParticleMixin", "features.render.world.clouds.WorldRendererMixin", "features.render.world.sky.BackgroundRendererMixin", "features.render.world.sky.ClientWorldMixin",