diff --git a/patches/net/minecraft/client/Minecraft.java.patch b/patches/net/minecraft/client/Minecraft.java.patch index 13ff259e92b..012ee51c409 100644 --- a/patches/net/minecraft/client/Minecraft.java.patch +++ b/patches/net/minecraft/client/Minecraft.java.patch @@ -352,6 +352,14 @@ if (integratedserver != null) { this.profiler.push("waitForServer"); +@@ -2175,6 +_,7 @@ + } + + private void updateLevelInEngines(@Nullable ClientLevel p_91325_) { ++ net.neoforged.neoforge.client.ClientHooks.onUpdateLevel(p_91325_); + this.levelRenderer.setLevel(p_91325_); + this.particleEngine.setLevel(p_91325_); + this.blockEntityRenderDispatcher.setLevel(p_91325_); @@ -2258,6 +_,7 @@ private void pickBlock() { diff --git a/patches/net/minecraft/client/renderer/LevelRenderer.java.patch b/patches/net/minecraft/client/renderer/LevelRenderer.java.patch index 82e084e8b4b..c2aca80494c 100644 --- a/patches/net/minecraft/client/renderer/LevelRenderer.java.patch +++ b/patches/net/minecraft/client/renderer/LevelRenderer.java.patch @@ -29,7 +29,7 @@ float f = this.minecraft.level.getRainLevel(1.0F) / (Minecraft.useFancyGraphics() ? 1.0F : 2.0F); if (!(f <= 0.0F)) { RandomSource randomsource = RandomSource.create((long)this.ticks * 312987231L); -@@ -942,9 +_,11 @@ +@@ -942,18 +_,23 @@ RenderSystem.clear(16640, Minecraft.ON_OSX); float f1 = p_109605_.getRenderDistance(); boolean flag1 = this.minecraft.level.effects().isFoggyAt(Mth.floor(d0), Mth.floor(d1)) || this.minecraft.gui.getBossOverlay().shouldCreateWorldFog(); @@ -41,7 +41,9 @@ profilerfiller.popPush("fog"); FogRenderer.setupFog(p_109604_, FogRenderer.FogMode.FOG_TERRAIN, Math.max(f1, 32.0F), flag1, f); profilerfiller.popPush("terrain_setup"); -@@ -953,7 +_,9 @@ + this.setupRender(p_109604_, frustum, flag, this.minecraft.player.isSpectator()); + profilerfiller.popPush("compile_sections"); ++ net.neoforged.neoforge.client.ClientHooks.onCompileSectionsPre(); this.compileSections(p_109604_); profilerfiller.popPush("terrain"); this.renderSectionLayer(RenderType.solid(), d0, d1, d2, p_254120_, p_323920_); @@ -70,13 +72,13 @@ multibuffersource = multibuffersource$buffersource; } -@@ -1027,12 +_,14 @@ +@@ -1027,12 +_,13 @@ multibuffersource$buffersource.endBatch(RenderType.entityCutout(TextureAtlas.LOCATION_BLOCKS)); multibuffersource$buffersource.endBatch(RenderType.entityCutoutNoCull(TextureAtlas.LOCATION_BLOCKS)); multibuffersource$buffersource.endBatch(RenderType.entitySmoothCutout(TextureAtlas.LOCATION_BLOCKS)); + net.neoforged.neoforge.client.ClientHooks.dispatchRenderStage(net.neoforged.neoforge.client.event.RenderLevelStageEvent.Stage.AFTER_ENTITIES, this, posestack, p_254120_, p_323920_, this.ticks, p_109604_, frustum); profilerfiller.popPush("blockentities"); - +- for (SectionRenderDispatcher.RenderSection sectionrenderdispatcher$rendersection : this.visibleSections) { List list = sectionrenderdispatcher$rendersection.getCompiled().getRenderableBlockEntities(); if (!list.isEmpty()) { @@ -153,6 +155,14 @@ } this.minecraft.debugRenderer.render(posestack, multibuffersource$buffersource, d0, d1, d2); +@@ -1133,6 +_,7 @@ + multibuffersource$buffersource.endBatch(RenderType.entityGlint()); + multibuffersource$buffersource.endBatch(RenderType.entityGlintDirect()); + multibuffersource$buffersource.endBatch(RenderType.waterMask()); ++ net.neoforged.neoforge.client.ClientHooks.onRenderTranslucentPre(p_254120_, p_323920_); + this.renderBuffers.crumblingBufferSource().endBatch(); + if (this.transparencyChain != null) { + multibuffersource$buffersource.endBatch(RenderType.lines()); @@ -1147,9 +_,13 @@ this.particlesTarget.copyDepthFrom(this.minecraft.getMainRenderTarget()); RenderStateShard.PARTICLES_TARGET.setupRenderState(); diff --git a/patches/net/minecraft/world/level/chunk/LevelChunk.java.patch b/patches/net/minecraft/world/level/chunk/LevelChunk.java.patch index a47b0de44e2..93fe60cd255 100644 --- a/patches/net/minecraft/world/level/chunk/LevelChunk.java.patch +++ b/patches/net/minecraft/world/level/chunk/LevelChunk.java.patch @@ -78,7 +78,15 @@ } else { CompoundTag compoundtag = this.pendingBlockEntities.get(p_62932_); if (compoundtag != null) { -@@ -436,6 +_,7 @@ +@@ -431,11 +_,15 @@ + if (this.isInLevel()) { + BlockEntity blockentity = this.blockEntities.remove(p_62919_); + if (blockentity != null) { ++ if (this.level.isClientSide) { ++ net.neoforged.neoforge.client.ClientHooks.onBlockEntityRemoved(blockentity); ++ } + if (this.level instanceof ServerLevel serverlevel) { + this.removeGameEventListener(blockentity, serverlevel); } blockentity.setRemoved(); diff --git a/src/main/java/net/neoforged/neoforge/client/ClientHooks.java b/src/main/java/net/neoforged/neoforge/client/ClientHooks.java index 005dd1e25b8..a933b940e75 100644 --- a/src/main/java/net/neoforged/neoforge/client/ClientHooks.java +++ b/src/main/java/net/neoforged/neoforge/client/ClientHooks.java @@ -137,6 +137,7 @@ import net.neoforged.fml.ModLoader; import net.neoforged.fml.common.EventBusSubscriber; import net.neoforged.fml.common.asm.enumextension.ExtensionInfo; +import net.neoforged.neoforge.client.block.CacheableBERenderingPipeline; import net.neoforged.neoforge.client.entity.animation.json.AnimationTypeManager; import net.neoforged.neoforge.client.event.AddSectionGeometryEvent; import net.neoforged.neoforge.client.event.CalculateDetachedCameraDistanceEvent; @@ -791,6 +792,22 @@ public static void registerShaders(RegisterShadersEvent event) throws IOExceptio } } + public static void onBlockEntityRemoved(BlockEntity blockEntity) { + CacheableBERenderingPipeline.getInstance().blockRemoved(blockEntity); + } + + public static void onRenderTranslucentPre(Matrix4f frustumMatrix, Matrix4f projectionMatrix) { + CacheableBERenderingPipeline.getInstance().render(frustumMatrix, projectionMatrix); + } + + public static void onCompileSectionsPre() { + CacheableBERenderingPipeline.getInstance().runTasks(); + } + + public static void onUpdateLevel(ClientLevel level) { + CacheableBERenderingPipeline.updateLevel(level); + } + public static Font getTooltipFont(ItemStack stack, Font fallbackFont) { Font stackFont = IClientItemExtensions.of(stack).getFont(stack, IClientItemExtensions.FontContext.TOOLTIP); return stackFont == null ? fallbackFont : stackFont; diff --git a/src/main/java/net/neoforged/neoforge/client/FullyBufferedBufferSource.java b/src/main/java/net/neoforged/neoforge/client/FullyBufferedBufferSource.java new file mode 100644 index 00000000000..76319f02a97 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/client/FullyBufferedBufferSource.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.client; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.BufferBuilder; +import com.mojang.blaze3d.vertex.ByteBufferBuilder; +import com.mojang.blaze3d.vertex.MeshData; +import com.mojang.blaze3d.vertex.VertexBuffer; +import com.mojang.blaze3d.vertex.VertexConsumer; +import it.unimi.dsi.fastutil.objects.Reference2IntMap; +import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; +import javax.annotation.ParametersAreNonnullByDefault; +import net.minecraft.MethodsReturnNonnullByDefault; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; + +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +public class FullyBufferedBufferSource extends MultiBufferSource.BufferSource implements AutoCloseable { + private final Map byteBuffers = new HashMap<>(); + private final Map bufferBuilders = new HashMap<>(); + private final Reference2IntMap indexCountMap = new Reference2IntOpenHashMap<>(); + private final Map meshSorts = new HashMap<>(); + + public FullyBufferedBufferSource() { + super(null, null); + } + + private ByteBufferBuilder getByteBuffer(RenderType renderType) { + return byteBuffers.computeIfAbsent(renderType, it -> new ByteBufferBuilder(786432)); + } + + @Override + public VertexConsumer getBuffer(RenderType renderType) { + return bufferBuilders.computeIfAbsent( + renderType, + it -> new BufferBuilder(getByteBuffer(it), it.mode, it.format)); + } + + public boolean isEmpty() { + return !bufferBuilders.isEmpty() && bufferBuilders.values().stream().noneMatch(it -> it.vertices > 0); + } + + @Override + public void endBatch(RenderType renderType) {} + + @Override + public void endLastBatch() {} + + @Override + public void endBatch() {} + + public void upload( + Function vertexBufferGetter, + Function byteBufferSupplier, + Consumer runner) { + for (RenderType renderType : bufferBuilders.keySet()) { + runner.accept(() -> { + BufferBuilder bufferBuilder = bufferBuilders.get(renderType); + ByteBufferBuilder byteBuffer = byteBuffers.get(renderType); + int compiledVertices = bufferBuilder.vertices * renderType.format.getVertexSize(); + if (compiledVertices >= 0) { + MeshData mesh = bufferBuilder.build(); + indexCountMap.put(renderType, renderType.mode.indexCount(bufferBuilder.vertices)); + if (mesh != null) { + if (renderType.sortOnUpload) { + MeshData.SortState sortState = mesh.sortQuads( + byteBufferSupplier.apply(renderType), + RenderSystem.getVertexSorting()); + meshSorts.put( + renderType, + sortState); + } + VertexBuffer vertexBuffer = vertexBufferGetter.apply(renderType); + vertexBuffer.bind(); + vertexBuffer.upload(mesh); + VertexBuffer.unbind(); + } + } + byteBuffer.close(); + bufferBuilders.remove(renderType); + byteBuffers.remove(renderType); + }); + } + } + + public void close(RenderType renderType) { + ByteBufferBuilder builder = byteBuffers.get(renderType); + builder.close(); + } + + public Reference2IntMap getIndexCountMap() { + return indexCountMap; + } + + public Map getMeshSorts() { + return meshSorts; + } + + public void close() { + byteBuffers.keySet().forEach(this::close); + } +} diff --git a/src/main/java/net/neoforged/neoforge/client/block/CacheableBERenderingPipeline.java b/src/main/java/net/neoforged/neoforge/client/block/CacheableBERenderingPipeline.java new file mode 100644 index 00000000000..c1bf064da97 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/client/block/CacheableBERenderingPipeline.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.client.block; + +import java.util.ArrayDeque; +import java.util.HashMap; +import java.util.Map; +import java.util.Queue; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.neoforged.neoforge.client.extensions.IBlockEntityRendererExtension; +import org.jetbrains.annotations.Nullable; +import org.joml.Matrix4f; + +public class CacheableBERenderingPipeline { + @Nullable + private static CacheableBERenderingPipeline instance; + private final ClientLevel level; + private final Queue pendingCompiles = new ArrayDeque<>(); + private final Queue pendingUploads = new ArrayDeque<>(); + private final Map regions = new HashMap<>(); + private boolean valid = true; + + public CachedRegion getRenderRegion(ChunkPos chunkPos) { + if (regions.containsKey(chunkPos)) { + return regions.get(chunkPos); + } + CachedRegion region = new CachedRegion(chunkPos, this); + regions.put(chunkPos, region); + return region; + } + + public CacheableBERenderingPipeline(ClientLevel level) { + this.level = level; + } + + public void runTasks() { + while (!pendingCompiles.isEmpty() && valid) { + pendingCompiles.poll().run(); + } + while (!pendingUploads.isEmpty() && valid) { + pendingUploads.poll().run(); + } + } + + /** + * Updates the rendering pipeline instance with a new level context. + * + * @param level The new ClientLevel instance that the rendering pipeline should be updated to use. + */ + public static void updateLevel(ClientLevel level) { + if (instance != null) { + instance.releaseBuffers(); + } + instance = new CacheableBERenderingPipeline(level); + } + + /** + * Notifies the pipeline that a {@link BlockEntity} has been removed. + * This method will be automatically called when a {@link BlockEntity} has been removed. + * + * @param be The removed {@link BlockEntity} + */ + public void blockRemoved(BlockEntity be) { + IBlockEntityRendererExtension renderer = Minecraft.getInstance() + .getBlockEntityRenderDispatcher() + .getRenderer(be); + if (renderer == null) return; + ChunkPos chunkPos = new ChunkPos(be.getBlockPos()); + getRenderRegion(chunkPos).blockRemoved(be); + } + + /** + * Notifies the pipeline that a {@link BlockEntity} has been updated and the cache should be rebuilt. + * + * @param be The updated {@link BlockEntity} + */ + public void update(BlockEntity be) { + BlockEntityRenderer renderer = Minecraft.getInstance() + .getBlockEntityRenderDispatcher() + .getRenderer(be); + if (renderer == null) return; + ChunkPos chunkPos = new ChunkPos(be.getBlockPos()); + getRenderRegion(chunkPos).update(be); + } + + public void submitUploadTask(Runnable task) { + pendingUploads.add(task); + } + + public void submitCompileTask(Runnable task) { + pendingCompiles.add(task); + } + + /** + * Releases all buffers in use and mark current pipeline instance as invalid. + */ + public void releaseBuffers() { + regions.values().forEach(CachedRegion::releaseBuffers); + valid = false; + } + + public void render(Matrix4f frustumMatrix, Matrix4f projectionMatrix) { + regions.values().forEach(it -> it.render(frustumMatrix, projectionMatrix)); + } + + /** + * Retrieves the current instance of the CacheableBERenderingPipeline. + * + * @return The current instance of the CacheableBERenderingPipeline, + * or null if there has no {@link ClientLevel} in current {@link Minecraft} client. + */ + @Nullable + public static CacheableBERenderingPipeline getInstance() { + return instance; + } +} diff --git a/src/main/java/net/neoforged/neoforge/client/block/CachedRegion.java b/src/main/java/net/neoforged/neoforge/client/block/CachedRegion.java new file mode 100644 index 00000000000..0ad3382d34e --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/client/block/CachedRegion.java @@ -0,0 +1,245 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.client.block; + +import com.mojang.blaze3d.platform.Window; +import com.mojang.blaze3d.shaders.Uniform; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.ByteBufferBuilder; +import com.mojang.blaze3d.vertex.MeshData; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexBuffer; +import com.mojang.blaze3d.vertex.VertexFormat; +import com.mojang.blaze3d.vertex.VertexSorting; +import it.unimi.dsi.fastutil.objects.Reference2IntMap; +import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.LevelRenderer; +import net.minecraft.client.renderer.LightTexture; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.ShaderInstance; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.phys.Vec3; +import net.neoforged.neoforge.client.FullyBufferedBufferSource; +import org.jetbrains.annotations.Nullable; +import org.joml.Matrix4f; + +public class CachedRegion { + private final ChunkPos chunkPos; + private Map buffers = new HashMap<>(); + private final Map sortBuffers = new HashMap<>(); + private Map meshSortings = new HashMap<>(); + private Reference2IntMap indexCountMap = new Reference2IntOpenHashMap<>(); + private final Set blockEntities = new HashSet<>(); + private final CacheableBERenderingPipeline pipeline; + private final Minecraft minecraft = Minecraft.getInstance(); + @Nullable + private RebuildTask lastRebuildTask; + + private boolean isEmpty = true; + + public CachedRegion(ChunkPos chunkPos, CacheableBERenderingPipeline pipeline) { + this.chunkPos = chunkPos; + this.pipeline = pipeline; + } + + /** + * Updates the block entities collection and triggers a rebuild of the region. + *

+ * + * @see CacheableBERenderingPipeline#update(BlockEntity) + * @param be The block entity to update. + */ + public void update(BlockEntity be) { + if (lastRebuildTask != null) { + lastRebuildTask.cancel(); + } + blockEntities.removeIf(BlockEntity::isRemoved); + if (be.isRemoved()) { + blockEntities.remove(be); + pipeline.submitCompileTask(new RebuildTask()); + return; + } + blockEntities.add(be); + pipeline.submitCompileTask(new RebuildTask()); + } + + /** + * Handles the removal of a block entity from the system and initiates a cache rebuild. + *

+ * When a block entity is removed, this method is called to update the internal state of the system. + * It cancels any ongoing rebuild tasks, removes the specified block entity from the collection, + * cleans up any other removed block entities, and then submits a new rebuild task to the pipeline. + * + * @see CacheableBERenderingPipeline#blockRemoved(BlockEntity) + * @param be The block entity that has been removed. + */ + public void blockRemoved(BlockEntity be) { + if (lastRebuildTask != null) { + lastRebuildTask.cancel(); + } + blockEntities.remove(be); + blockEntities.removeIf(BlockEntity::isRemoved); + pipeline.submitCompileTask(new RebuildTask()); + } + + public void render(Matrix4f frustumMatrix, Matrix4f projectionMatrix) { + renderInternal(frustumMatrix, projectionMatrix, buffers.keySet()); + } + + public VertexBuffer getBuffer(RenderType renderType) { + if (buffers.containsKey(renderType)) { + return buffers.get(renderType); + } + VertexBuffer vb = new VertexBuffer(VertexBuffer.Usage.STATIC); + buffers.put(renderType, vb); + return vb; + } + + private ByteBufferBuilder requestSortBuffer(RenderType renderType) { + if (sortBuffers.containsKey(renderType)) { + return sortBuffers.get(renderType); + } + ByteBufferBuilder builder = new ByteBufferBuilder(4096); + sortBuffers.put(renderType, builder); + return builder; + } + + private void renderInternal( + Matrix4f frustumMatrix, + Matrix4f projectionMatrix, + Collection renderTypes) { + if (isEmpty) return; + RenderSystem.enableBlend(); + Window window = Minecraft.getInstance().getWindow(); + Vec3 cameraPosition = minecraft.gameRenderer.getMainCamera().getPosition(); + int renderDistance = Minecraft.getInstance().options.getEffectiveRenderDistance() * 16; + if (cameraPosition.distanceTo(new Vec3(chunkPos.x * 16, cameraPosition.y, chunkPos.z * 16)) > renderDistance) { + return; + } + List renderingOrders = new ArrayList<>(renderTypes); + renderingOrders.sort(Comparator.comparingInt(a -> (a.sortOnUpload ? 1 : 0))); + for (RenderType renderType : renderingOrders) { + VertexBuffer vb = buffers.get(renderType); + if (vb == null) continue; + renderLayer(renderType, vb, frustumMatrix, projectionMatrix, cameraPosition, window); + } + } + + public void releaseBuffers() { + buffers.values().forEach(VertexBuffer::close); + sortBuffers.values().forEach(ByteBufferBuilder::close); + } + + private void renderLayer( + RenderType renderType, + VertexBuffer vertexBuffer, + Matrix4f frustumMatrix, + Matrix4f projectionMatrix, + Vec3 cameraPosition, + Window window) { + int indexCount = indexCountMap.getInt(renderType); + if (indexCount <= 0) return; + renderType.setupRenderState(); + ShaderInstance shader = RenderSystem.getShader(); + shader.setDefaultUniforms(VertexFormat.Mode.QUADS, frustumMatrix, projectionMatrix, window); + Uniform uniform = shader.CHUNK_OFFSET; + if (uniform != null) { + uniform.set( + (float) -cameraPosition.x, + (float) -cameraPosition.y, + (float) -cameraPosition.z); + } + vertexBuffer.bind(); + if (renderType.sortOnUpload) { + MeshData.SortState sortState = this.meshSortings.get(renderType); + if (sortState != null) { + ByteBufferBuilder.Result result = sortState.buildSortedIndexBuffer( + this.requestSortBuffer(renderType), + VertexSorting.byDistance(cameraPosition.toVector3f())); + if (result != null) { + vertexBuffer.uploadIndexBuffer(result); + } + } + } + vertexBuffer.drawWithShader(frustumMatrix, projectionMatrix, shader); + VertexBuffer.unbind(); + if (uniform != null) { + uniform.set(0.0F, 0.0F, 0.0F); + } + renderType.clearRenderState(); + } + + private class RebuildTask implements Runnable { + private boolean cancelled = false; + + @Override + public void run() { + lastRebuildTask = this; + PoseStack poseStack = new PoseStack(); + CachedRegion.this.isEmpty = true; + FullyBufferedBufferSource bufferSource = new FullyBufferedBufferSource(); + float partialTick = Minecraft.getInstance().getTimer().getGameTimeDeltaPartialTick(false); + for (BlockEntity be : new ArrayList<>(blockEntities)) { + if (cancelled) { + bufferSource.close(); + return; + } + BlockEntityRenderer renderer = Minecraft.getInstance() + .getBlockEntityRenderDispatcher() + .getRenderer(be); + if (renderer == null) continue; + Level level = be.getLevel(); + int packedLight; + if (level != null) { + packedLight = LevelRenderer.getLightColor(level, be.getBlockPos()); + } else { + packedLight = LightTexture.FULL_BRIGHT; + } + poseStack.pushPose(); + BlockPos pos = be.getBlockPos(); + poseStack.translate( + pos.getX(), + pos.getY(), + pos.getZ()); + renderer.renderCached( + be, + poseStack, + bufferSource, + partialTick, + packedLight, + OverlayTexture.NO_OVERLAY); + poseStack.popPose(); + } + CachedRegion.this.isEmpty = bufferSource.isEmpty(); + bufferSource.upload( + CachedRegion.this::getBuffer, + CachedRegion.this::requestSortBuffer, + pipeline::submitUploadTask); + + CachedRegion.this.meshSortings = bufferSource.getMeshSorts(); + CachedRegion.this.indexCountMap = bufferSource.getIndexCountMap(); + lastRebuildTask = null; + } + + void cancel() { + cancelled = true; + } + } +} diff --git a/src/main/java/net/neoforged/neoforge/client/block/package-info.java b/src/main/java/net/neoforged/neoforge/client/block/package-info.java new file mode 100644 index 00000000000..6589ff44eb2 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/client/block/package-info.java @@ -0,0 +1,13 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault +package net.neoforged.neoforge.client.block; + +import javax.annotation.ParametersAreNonnullByDefault; +import net.minecraft.FieldsAreNonnullByDefault; +import net.minecraft.MethodsReturnNonnullByDefault; diff --git a/src/main/java/net/neoforged/neoforge/client/extensions/IBlockEntityRendererExtension.java b/src/main/java/net/neoforged/neoforge/client/extensions/IBlockEntityRendererExtension.java index 99e7faef593..92daa6071c5 100644 --- a/src/main/java/net/neoforged/neoforge/client/extensions/IBlockEntityRendererExtension.java +++ b/src/main/java/net/neoforged/neoforge/client/extensions/IBlockEntityRendererExtension.java @@ -5,6 +5,8 @@ package net.neoforged.neoforge.client.extensions; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.phys.AABB; @@ -20,4 +22,12 @@ public interface IBlockEntityRendererExtension { default AABB getRenderBoundingBox(T blockEntity) { return new AABB(blockEntity.getBlockPos()); } + + default void renderCached( + T blockEntity, + PoseStack poseStack, + MultiBufferSource.BufferSource bufferSource, + float partialTick, + int packedLight, + int packedOverlay) {} } diff --git a/src/main/resources/META-INF/accesstransformer.cfg b/src/main/resources/META-INF/accesstransformer.cfg index 11913873b90..955c379e5b1 100644 --- a/src/main/resources/META-INF/accesstransformer.cfg +++ b/src/main/resources/META-INF/accesstransformer.cfg @@ -501,3 +501,6 @@ public net.minecraft.world.item.enchantment.Enchantment locationContext(Lnet/min public net.minecraft.world.item.enchantment.Enchantment entityContext(Lnet/minecraft/server/level/ServerLevel;ILnet/minecraft/world/entity/Entity;Lnet/minecraft/world/phys/Vec3;)Lnet/minecraft/world/level/storage/loot/LootContext; public net.minecraft.world.item.enchantment.Enchantment blockHitContext(Lnet/minecraft/server/level/ServerLevel;ILnet/minecraft/world/entity/Entity;Lnet/minecraft/world/phys/Vec3;Lnet/minecraft/world/level/block/state/BlockState;)Lnet/minecraft/world/level/storage/loot/LootContext; public net.minecraft.world.item.enchantment.Enchantment applyEffects(Ljava/util/List;Lnet/minecraft/world/level/storage/loot/LootContext;Ljava/util/function/Consumer;)V + +#Cached BlockEntityRenderer pipeline +public com.mojang.blaze3d.vertex.BufferBuilder vertices \ No newline at end of file diff --git a/tests/src/main/java/net/neoforged/neoforge/debug/client/CachedBERTests.java b/tests/src/main/java/net/neoforged/neoforge/debug/client/CachedBERTests.java new file mode 100644 index 00000000000..8dcb4489f81 --- /dev/null +++ b/tests/src/main/java/net/neoforged/neoforge/debug/client/CachedBERTests.java @@ -0,0 +1,201 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.debug.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.serialization.MapCodec; +import java.util.function.Supplier; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.block.BlockRenderDispatcher; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; +import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderers; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.Component; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.ItemInteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.phys.BlockHitResult; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent; +import net.neoforged.neoforge.client.block.CacheableBERenderingPipeline; +import net.neoforged.neoforge.client.model.data.ModelData; +import net.neoforged.neoforge.registries.DeferredBlock; +import net.neoforged.neoforge.registries.DeferredRegister; +import net.neoforged.testframework.DynamicTest; +import net.neoforged.testframework.annotation.ForEachTest; +import net.neoforged.testframework.annotation.TestHolder; +import org.jetbrains.annotations.Nullable; + +@ForEachTest(side = Dist.CLIENT, groups = { "client.event", "event" }) +public class CachedBERTests { + public static final DeferredRegister.Blocks BLOCKS = DeferredRegister.Blocks.createBlocks("neotests_cached_ber"); + public static final DeferredRegister> BLOCK_ENTITY_TYPES = DeferredRegister.create(Registries.BLOCK_ENTITY_TYPE, "neotests_cached_ber"); + + public static final DeferredBlock THE_BLOCK = BLOCKS.register( + "not_enough_vertexes", + () -> new TheBlock( + BlockBehaviour.Properties.of() + .noCollission() + .noOcclusion() + .lightLevel(state -> 15))); + + public static final Supplier> THE_BE = BLOCK_ENTITY_TYPES.register( + "not_enough_vertexes", + () -> BlockEntityType.Builder.of( + TheBlockEntity::new, + THE_BLOCK.get()) + .build(null)); + + @TestHolder(description = "Register a block with cached BER which adds lots of vertexes") + static void registerBlock(final DynamicTest test) { + BLOCKS.register(test.framework().modEventBus()); + BLOCK_ENTITY_TYPES.register(test.framework().modEventBus()); + test.framework().modEventBus().addListener(CachedBERTests::clientSetup); + } + + static private void clientSetup(final FMLClientSetupEvent event) { + BlockEntityRenderers.register(THE_BE.get(), TheRenderer::new); + } + + public static class TheBlock extends BaseEntityBlock { + protected TheBlock(Properties p_49224_) { + super(p_49224_); + registerDefaultState(getStateDefinition().any().setValue(BlockStateProperties.ENABLED, false)); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(BlockStateProperties.ENABLED); + } + + @Override + protected ItemInteractionResult useItemOn(ItemStack p_316304_, BlockState state, Level level, BlockPos pos, Player p_316132_, InteractionHand p_316595_, BlockHitResult p_316140_) { + level.setBlockAndUpdate( + pos, + state.setValue(BlockStateProperties.ENABLED, !state.getValue(BlockStateProperties.ENABLED))); + if (!level.isClientSide) return ItemInteractionResult.SUCCESS; + BlockEntity be = level.getBlockEntity(pos); + if (be == null) return ItemInteractionResult.SUCCESS; + if (!level.getBlockState(pos).getValue(BlockStateProperties.ENABLED)) { + CacheableBERenderingPipeline.getInstance().update(be); + Minecraft.getInstance().player.sendSystemMessage(Component.literal("RenderMode: BER")); + } else { + CacheableBERenderingPipeline.getInstance().update(be); + Minecraft.getInstance().player.sendSystemMessage(Component.literal("RenderMode: Cached BER")); + } + return ItemInteractionResult.SUCCESS; + } + + @Override + protected RenderShape getRenderShape(BlockState p_49232_) { + return RenderShape.MODEL; + } + + @Override + protected MapCodec codec() { + return Block.simpleCodec(TheBlock::new); + } + + @Override + public @Nullable BlockEntity newBlockEntity(BlockPos p_153215_, BlockState p_153216_) { + return new TheBlockEntity(p_153215_, p_153216_); + } + } + + public static class TheBlockEntity extends BlockEntity { + public TheBlockEntity(BlockPos p_155229_, BlockState p_155230_) { + super(THE_BE.get(), p_155229_, p_155230_); + } + } + + public static class TheRenderer implements BlockEntityRenderer { + public TheRenderer(BlockEntityRendererProvider.Context ctx) {} + + @Override + public void render( + TheBlockEntity blockEntity, + float partialTick, + PoseStack poseStack, + MultiBufferSource bufferSource, + int packedLight, + int packedOverlay) { + Level level = blockEntity.getLevel(); + BlockPos pos = blockEntity.getBlockPos(); + if (level == null) return; + BlockState blockState = level.getBlockState(pos); + if (!blockState.is(THE_BLOCK.get())) return; + if (blockState.getValue(BlockStateProperties.ENABLED)) return; + renderManyBlocks(poseStack, bufferSource, packedLight, packedOverlay); + } + + private void renderManyBlocks(PoseStack poseStack, MultiBufferSource bufferSource, int packedLight, int packedOverlay) { + BlockRenderDispatcher dispatcher = Minecraft.getInstance().getBlockRenderer(); + for (int dx = 0; dx < 32; dx++) { + for (int dz = 0; dz < 32; dz++) { + poseStack.pushPose(); + poseStack.translate(dx, 2, dz); + dispatcher.renderSingleBlock( + Blocks.TORCH.defaultBlockState(), + poseStack, + bufferSource, + packedLight, + packedOverlay, + ModelData.EMPTY, + RenderType.cutout()); + poseStack.popPose(); + } + } + for (int dx = 0; dx < 32; dx++) { + for (int dz = 0; dz < 32; dz++) { + poseStack.pushPose(); + poseStack.translate(dx, 1, dz); + dispatcher.renderSingleBlock( + Blocks.BLACK_STAINED_GLASS.defaultBlockState(), + poseStack, + bufferSource, + packedLight, + packedOverlay, + ModelData.EMPTY, + RenderType.translucent()); + poseStack.popPose(); + } + } + } + + @Override + public void renderCached( + TheBlockEntity blockEntity, + PoseStack poseStack, + MultiBufferSource.BufferSource bufferSource, + float partialTick, + int packedLight, + int packedOverlay) { + Level level = blockEntity.getLevel(); + BlockPos pos = blockEntity.getBlockPos(); + if (level == null) return; + BlockState blockState = level.getBlockState(pos); + if (!blockState.is(THE_BLOCK.get())) return; + if (!blockState.getValue(BlockStateProperties.ENABLED)) return; + renderManyBlocks(poseStack, bufferSource, packedLight, packedOverlay); + } + } +}