mirror of
https://github.com/GTNewHorizons/Angelica.git
synced 2026-11-08 12:35:01 +00:00
Move deferred chunk meshing back off the main thread (#2306)
This commit is contained in:
1 parent
91e67f8d5d
commit
ed6dd12db7
11 files changed
+638
-247
No files matched your search
+5
-4
@@ -44,6 +44,7 @@ import java.util.List;
|
||||
|
||||
public class AngelicaChunkBuildContext extends ChunkBuildContext {
|
||||
public static final int NUM_PASSES = 2;
|
||||
public static final int INTS_PER_VERTEX = 8;
|
||||
|
||||
// Yoinked from Celeritas
|
||||
private static final int[] REVERSED_QUAD_ORDER = { 0, 3, 2, 1 };
|
||||
@@ -121,7 +122,7 @@ public class AngelicaChunkBuildContext extends ChunkBuildContext {
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void copyRawBuffer(int[] rawBuffer, int vertexCount, int[] vertexStates,
|
||||
public void copyRawBuffer(int[] rawBuffer, int vertexOffset, int vertexCount, int[] vertexStates,
|
||||
int[] shaderOverrideBlockIds,
|
||||
ChunkBuildBuffers buffers, Material material, boolean isShaderPackOverride,
|
||||
boolean blockAllowsSmoothLighting) {
|
||||
@@ -144,7 +145,7 @@ public class AngelicaChunkBuildContext extends ChunkBuildContext {
|
||||
final boolean shouldApplyDiffuse = !BlockRenderingSettings.INSTANCE.shouldDisableDirectionalShading();
|
||||
final ChunkColorWriter colorEncoder = separateAo ? ChunkColorWriter.SEPARATE_AO : ChunkColorWriter.EMBEDDIUM;
|
||||
|
||||
int ptr = 0;
|
||||
int ptr = vertexOffset * INTS_PER_VERTEX;
|
||||
final int numQuads = vertexCount / 4;
|
||||
|
||||
final int blockX = blockRenderContext.localPosX;
|
||||
@@ -156,7 +157,7 @@ public class AngelicaChunkBuildContext extends ChunkBuildContext {
|
||||
|
||||
final short originalBlockId = blockRenderContext.blockId;
|
||||
|
||||
int stateIdx = 0;
|
||||
int stateIdx = vertexOffset;
|
||||
materialSelector.reset(material, isShaderPackOverride, useRenderPassOptimization);
|
||||
this.fluidQuadLightFace = null;
|
||||
|
||||
@@ -250,7 +251,7 @@ public class AngelicaChunkBuildContext extends ChunkBuildContext {
|
||||
final Material correctMaterial = materialSelector.select(facing, sprite, hasVertexTransparency);
|
||||
final var builder = buffers.get(correctMaterial);
|
||||
|
||||
final int shaderOverrideBlockId = shaderOverrideBlockIds[quadIdx * 4];
|
||||
final int shaderOverrideBlockId = shaderOverrideBlockIds[vertexOffset + quadIdx * 4];
|
||||
blockRenderContext.blockId = shaderOverrideBlockId == StateAwareTessellator.UNMAPPED_SHADER_BLOCK_ID
|
||||
? -1 : shaderOverrideBlockId != -1 ? (short) shaderOverrideBlockId : originalBlockId;
|
||||
|
||||
|
||||
+164
-94
@@ -1,7 +1,9 @@
|
||||
package com.gtnewhorizons.angelica.rendering.celeritas;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.blockpos.BlockPos;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.TessellatorManager;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
|
||||
import com.gtnewhorizons.angelica.rendering.RenderThreadContext;
|
||||
import com.gtnewhorizons.angelica.rendering.StateAwareTessellator;
|
||||
import com.gtnewhorizons.angelica.mixins.interfaces.OverridesGetDistanceFrom;
|
||||
import com.gtnewhorizons.angelica.rendering.TileEntityRenderBoundsRegistry;
|
||||
@@ -254,6 +256,8 @@ public abstract class AngelicaChunkBuilderMeshingTask extends ChunkBuilderTask<C
|
||||
|
||||
// Check for shader pack override
|
||||
final BlockRenderLayer override = blockTypeIds != null ? blockTypeIds.get(block) : null;
|
||||
int deferredPasses = 0;
|
||||
Material deferredOverride = null;
|
||||
|
||||
for (int pass = 0; pass < AngelicaChunkBuildContext.NUM_PASSES; pass++) {
|
||||
final boolean canRender;
|
||||
@@ -273,13 +277,17 @@ public abstract class AngelicaChunkBuilderMeshingTask extends ChunkBuilderTask<C
|
||||
if (canRender) {
|
||||
final boolean isShaderPackOverride = materialOverride != null;
|
||||
if (!canRenderOffThread) {
|
||||
deferredBlocks.add(new DeferredBlock(x, y, z, block, meta, pass, materialOverride, isShaderPackOverride));
|
||||
deferredPasses |= 1 << pass;
|
||||
deferredOverride = materialOverride;
|
||||
continue;
|
||||
}
|
||||
|
||||
renderBlock(block, meta, x, y, z, pass, tessellator, renderBlocks, buffers, buildContext, blockRenderContext, minX, minY, minZ, materialOverride, isShaderPackOverride, teMap, currentTick);
|
||||
}
|
||||
}
|
||||
if (deferredPasses != 0) {
|
||||
deferredBlocks.add(new DeferredBlock(x, y, z, block, meta, deferredPasses, deferredOverride));
|
||||
}
|
||||
|
||||
if (block.isOpaqueCube()) {
|
||||
occluder.markOpaque(x, y, z);
|
||||
@@ -301,7 +309,8 @@ public abstract class AngelicaChunkBuilderMeshingTask extends ChunkBuilderTask<C
|
||||
throw new IllegalStateException("Deferred handoff requires the context WorldSlice for " + this.render);
|
||||
}
|
||||
final WorldSlice slice = buildContext.swapWorldSlice(scheduler.acquireSlice());
|
||||
final DeferredSectionMesh mesh = new DeferredSectionMesh(this, cancellationToken, buffers, slice, renderData, deferredBlocks, teMap, currentTick, this.important, System.nanoTime() - start);
|
||||
final long priority = (long) this.render.getSquaredDistanceFromBlockCenter((int) Math.floor(camera.x), (int) Math.floor(camera.y), (int) Math.floor(camera.z));
|
||||
final DeferredSectionMesh mesh = new DeferredSectionMesh(this, cancellationToken, buffers, slice, renderData, deferredBlocks, teMap, currentTick, this.important, priority, System.nanoTime() - start);
|
||||
scheduler.submit(mesh);
|
||||
handoffBuffers = null;
|
||||
|
||||
@@ -311,32 +320,10 @@ public abstract class AngelicaChunkBuilderMeshingTask extends ChunkBuilderTask<C
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Tracy.ENABLED) Tracy.beginZone(Z_MESH_FINALIZE);
|
||||
final Reference2ReferenceMap<TerrainRenderPass, BuiltSectionMeshParts> meshes;
|
||||
try {
|
||||
meshes = BuiltSectionMeshParts.groupFromBuildBuffers(buffers,
|
||||
(float) (camera.x - minX), (float) (camera.y - minY), (float) (camera.z - minZ));
|
||||
} finally {
|
||||
if (Tracy.ENABLED) Tracy.endZone();
|
||||
}
|
||||
|
||||
if (!meshes.isEmpty()) {
|
||||
renderData.hasBlockGeometry = true;
|
||||
if (Tracy.ENABLED) {
|
||||
long meshBytes = 0;
|
||||
for (BuiltSectionMeshParts parts : meshes.values()) {
|
||||
if (parts.vertexBuffer() != null) meshBytes += parts.vertexBuffer().getLength();
|
||||
}
|
||||
Tracy.zoneValue(meshBytes);
|
||||
}
|
||||
}
|
||||
|
||||
SmoothBiomeColorCache.clearActiveCache();
|
||||
tessellator.setTranslation(0, 0, 0);
|
||||
|
||||
CeleritasDebug.incrementChunkUpdateCounter();
|
||||
|
||||
return new ChunkBuildOutput(this.render, renderData, meshes, this.buildTime);
|
||||
return finalizeSection(buffers, renderData, minX, minY, minZ);
|
||||
|
||||
} catch (ReportedException ex) {
|
||||
throw fillCrashInfo(ex.getCrashReport(), region, blockPos);
|
||||
@@ -374,7 +361,7 @@ public abstract class AngelicaChunkBuilderMeshingTask extends ChunkBuilderTask<C
|
||||
return (mask[bit >>> 6] & (1L << bit)) != 0;
|
||||
}
|
||||
|
||||
ChunkJobResult<ChunkBuildOutput> completeDeferred(DeferredSectionMesh m, AngelicaChunkBuildContext ctx) {
|
||||
ChunkJobResult<ChunkBuildOutput> captureDeferred(DeferredSectionMesh m) {
|
||||
final long start = System.nanoTime();
|
||||
|
||||
final int minX = this.render.getOriginX();
|
||||
@@ -384,7 +371,10 @@ public abstract class AngelicaChunkBuilderMeshingTask extends ChunkBuilderTask<C
|
||||
final WorldSlice slice = m.slice;
|
||||
final BlockPos blockPos = new BlockPos(minX, minY, minZ);
|
||||
final Tessellator tessellator = Tessellator.instance;
|
||||
final StateAwareTessellator stateTessellator = (StateAwareTessellator) tessellator;
|
||||
final List<DeferredBlock> blocks = m.blocks;
|
||||
final DeferredSectionMesh.DeferredCapture capture = m.capture;
|
||||
final NbtConditionalIdMap<Block> teMap = m.teMap;
|
||||
|
||||
if (Tracy.ENABLED) {
|
||||
Tracy.beginZone(Z_MESH_DEFERRED_MAIN);
|
||||
@@ -392,51 +382,44 @@ public abstract class AngelicaChunkBuilderMeshingTask extends ChunkBuilderTask<C
|
||||
}
|
||||
|
||||
try {
|
||||
ctx.setupLightPipeline(slice, minX, minY, minZ);
|
||||
ctx.setupDynamicLights(minX, minY, minZ);
|
||||
slice.setRenderingBlock(null);
|
||||
|
||||
SmoothBiomeColorCache.setActiveCache(slice.getBiomeColorCache());
|
||||
((StateAwareTessellator)tessellator).angelica$setCeleritasMeshing(true);
|
||||
stateTessellator.angelica$setCeleritasMeshing(true);
|
||||
tessellator.setTranslation(-minX, -minY, -minZ);
|
||||
|
||||
final RenderBlocks renderBlocks = new RenderBlocks(slice);
|
||||
final BlockRenderContext blockRenderContext = ctx.getBlockRenderContext();
|
||||
|
||||
for (int i = 0, n = blocks.size(); i < n; i++) {
|
||||
final DeferredBlock deferred = blocks.get(i);
|
||||
blockPos.set(deferred.x(), deferred.y(), deferred.z());
|
||||
slice.setRenderingBlock(deferred.block());
|
||||
if (!deferred.isShaderPackOverride()) {
|
||||
setRenderPass(deferred.pass());
|
||||
if (!deferred.block().canRenderInPass(deferred.pass())) continue;
|
||||
final Block block = deferred.block();
|
||||
final int x = deferred.x(), y = deferred.y(), z = deferred.z();
|
||||
blockPos.set(x, y, z);
|
||||
slice.setRenderingBlock(block);
|
||||
for (int pass = 0; pass < AngelicaChunkBuildContext.NUM_PASSES; pass++) {
|
||||
if ((deferred.passMask() & (1 << pass)) == 0) continue;
|
||||
if (!deferred.isShaderPackOverride()) {
|
||||
setRenderPass(pass);
|
||||
if (!block.canRenderInPass(pass)) continue;
|
||||
}
|
||||
final int teBlockId = resolveTeBlockId(teMap, slice, block, deferred.meta(), x, y, z, m.buffers, pass, deferred.materialOverride(), m.currentTick);
|
||||
drawBlock(block, x, y, z, pass, tessellator, renderBlocks);
|
||||
if (tessellator.vertexCount > 0) {
|
||||
capture.add(i * AngelicaChunkBuildContext.NUM_PASSES + pass, tessellator.rawBuffer, tessellator.vertexCount, stateTessellator.angelica$getVertexStates(), stateTessellator.angelica$getShaderOverrideBlockIds(), teBlockId);
|
||||
}
|
||||
tessellator.reset();
|
||||
tessellator.isDrawing = false;
|
||||
}
|
||||
renderBlock(deferred.block(), deferred.meta(), deferred.x(), deferred.y(), deferred.z(), deferred.pass(),
|
||||
tessellator, renderBlocks, m.buffers, ctx, blockRenderContext, minX, minY, minZ,
|
||||
deferred.materialOverride(), deferred.isShaderPackOverride(), m.teMap, m.currentTick);
|
||||
}
|
||||
|
||||
if (Tracy.ENABLED) Tracy.beginZone(Z_MESH_FINALIZE);
|
||||
final Reference2ReferenceMap<TerrainRenderPass, BuiltSectionMeshParts> meshes;
|
||||
try {
|
||||
meshes = BuiltSectionMeshParts.groupFromBuildBuffers(m.buffers, (float) (camera.x - minX), (float) (camera.y - minY), (float) (camera.z - minZ));
|
||||
} finally {
|
||||
if (Tracy.ENABLED) Tracy.endZone();
|
||||
}
|
||||
|
||||
if (!meshes.isEmpty()) {
|
||||
m.renderData.hasBlockGeometry = true;
|
||||
}
|
||||
|
||||
CeleritasDebug.incrementChunkUpdateCounter();
|
||||
|
||||
return new ChunkJobResult.Success<>(new ChunkBuildOutput(this.render, m.renderData, meshes, this.buildTime), m.workerNanos + (System.nanoTime() - start));
|
||||
m.mainNanos = System.nanoTime() - start;
|
||||
return null;
|
||||
} catch (ReportedException ex) {
|
||||
return new ChunkJobResult.Failure<>(fillCrashInfo(ex.getCrashReport(), slice, blockPos));
|
||||
} catch (Throwable ex) {
|
||||
return new ChunkJobResult.Failure<>(fillCrashInfo(CrashReport.makeCrashReport(ex, "Encountered exception while building deferred chunk meshes"), slice, blockPos));
|
||||
} finally {
|
||||
((StateAwareTessellator)tessellator).angelica$setCeleritasMeshing(false);
|
||||
stateTessellator.angelica$setCeleritasMeshing(false);
|
||||
tessellator.setTranslation(0, 0, 0);
|
||||
SmoothBiomeColorCache.clearActiveCache();
|
||||
RenderPassHelper.resetWorldRenderPass();
|
||||
@@ -444,22 +427,105 @@ public abstract class AngelicaChunkBuilderMeshingTask extends ChunkBuilderTask<C
|
||||
}
|
||||
}
|
||||
|
||||
protected void renderBlock(Block block, int metadata, int x, int y, int z, int pass, Tessellator tessellator, RenderBlocks renderBlocks, ChunkBuildBuffers buffers, AngelicaChunkBuildContext buildContext, BlockRenderContext blockRenderContext, int originX, int originY, int originZ, Material materialOverride, boolean isShaderPackOverride, NbtConditionalIdMap<Block> teMap, long currentTick) {
|
||||
ChunkBuildOutput finishDeferred(DeferredSectionMesh m, AngelicaChunkBuildContext ctx) {
|
||||
final int minX = this.render.getOriginX();
|
||||
final int minY = this.render.getOriginY();
|
||||
final int minZ = this.render.getOriginZ();
|
||||
|
||||
final var blockMaterial = block.getMaterial();
|
||||
final boolean isFluid = blockMaterial == net.minecraft.block.material.Material.water || blockMaterial == net.minecraft.block.material.Material.lava;
|
||||
final WorldSlice slice = m.slice;
|
||||
final BlockPos blockPos = new BlockPos(minX, minY, minZ);
|
||||
final ChunkBuildBuffers buffers = m.buffers;
|
||||
final List<DeferredBlock> blocks = m.blocks;
|
||||
final DeferredSectionMesh.DeferredCapture capture = m.capture;
|
||||
final boolean bindSlice = !TessellatorManager.isOnMainThread();
|
||||
|
||||
// Use material override if provided, otherwise derive from pass
|
||||
// Lava is opaque - use solid to skip unnecessary alpha testing
|
||||
final Material passMaterial;
|
||||
if (materialOverride != null) {
|
||||
passMaterial = materialOverride;
|
||||
} else if (blockMaterial == net.minecraft.block.material.Material.lava) {
|
||||
passMaterial = AngelicaRenderPassConfiguration.SOLID_MATERIAL;
|
||||
} else {
|
||||
passMaterial = buffers.getRenderPassConfiguration().getMaterialForRenderType(BlockRenderLayer.fromVanillaPass(pass));
|
||||
try {
|
||||
ctx.setupLightPipeline(slice, minX, minY, minZ);
|
||||
ctx.setupDynamicLights(minX, minY, minZ);
|
||||
if (bindSlice) RenderThreadContext.set(slice);
|
||||
slice.setRenderingBlock(null);
|
||||
|
||||
final BlockRenderContext blockRenderContext = ctx.getBlockRenderContext();
|
||||
final int[] raw = capture.raw.elements();
|
||||
final int[] vertexStates = capture.vertexStates.elements();
|
||||
final int[] shaderBlockIds = capture.shaderBlockIds.elements();
|
||||
|
||||
for (int i = 0, n = capture.blockCount(); i < n; i++) {
|
||||
final int entry = capture.blockIndex.getInt(i);
|
||||
final DeferredBlock deferred = blocks.get(entry / AngelicaChunkBuildContext.NUM_PASSES);
|
||||
final Block block = deferred.block();
|
||||
final int x = deferred.x(), y = deferred.y(), z = deferred.z();
|
||||
blockPos.set(x, y, z);
|
||||
slice.setRenderingBlock(block);
|
||||
final Material passMaterial = resolvePassMaterial(block, entry % AngelicaChunkBuildContext.NUM_PASSES, buffers, deferred.materialOverride());
|
||||
final ContextAwareChunkVertexEncoder contextEncoder = prepareEncode(block, deferred.meta(), x, y, z, slice, buffers, blockRenderContext, passMaterial, capture.teBlockIds.getInt(i));
|
||||
encodeTessellated(raw, capture.blockStart.getInt(i), capture.blockVertexCount.getInt(i), vertexStates, shaderBlockIds, block, buffers, ctx, passMaterial, deferred.isShaderPackOverride(), contextEncoder);
|
||||
}
|
||||
|
||||
return finalizeSection(buffers, m.renderData, minX, minY, minZ);
|
||||
} catch (ReportedException ex) {
|
||||
throw fillCrashInfo(ex.getCrashReport(), slice, blockPos);
|
||||
} catch (Throwable ex) {
|
||||
throw fillCrashInfo(CrashReport.makeCrashReport(ex, "Encountered exception while building deferred chunk meshes"), slice, blockPos);
|
||||
} finally {
|
||||
if (bindSlice) RenderThreadContext.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private ChunkBuildOutput finalizeSection(ChunkBuildBuffers buffers, AngelicaBuiltRenderSectionData renderData, int minX, int minY, int minZ) {
|
||||
if (Tracy.ENABLED) Tracy.beginZone(Z_MESH_FINALIZE);
|
||||
final Reference2ReferenceMap<TerrainRenderPass, BuiltSectionMeshParts> meshes;
|
||||
try {
|
||||
meshes = BuiltSectionMeshParts.groupFromBuildBuffers(buffers, (float) (camera.x - minX), (float) (camera.y - minY), (float) (camera.z - minZ));
|
||||
} finally {
|
||||
if (Tracy.ENABLED) Tracy.endZone();
|
||||
}
|
||||
|
||||
if (!meshes.isEmpty()) {
|
||||
renderData.hasBlockGeometry = true;
|
||||
if (Tracy.ENABLED) {
|
||||
long meshBytes = 0;
|
||||
for (BuiltSectionMeshParts parts : meshes.values()) {
|
||||
if (parts.vertexBuffer() != null) meshBytes += parts.vertexBuffer().getLength();
|
||||
}
|
||||
Tracy.zoneValue(meshBytes);
|
||||
}
|
||||
}
|
||||
|
||||
CeleritasDebug.incrementChunkUpdateCounter();
|
||||
|
||||
return new ChunkBuildOutput(this.render, renderData, meshes, this.buildTime);
|
||||
}
|
||||
|
||||
protected void renderBlock(Block block, int metadata, int x, int y, int z, int pass, Tessellator tessellator, RenderBlocks renderBlocks, ChunkBuildBuffers buffers, AngelicaChunkBuildContext buildContext, BlockRenderContext blockRenderContext, int originX, int originY, int originZ, Material materialOverride, boolean isShaderPackOverride, NbtConditionalIdMap<Block> teMap, long currentTick) {
|
||||
final Material passMaterial = resolvePassMaterial(block, pass, buffers, materialOverride);
|
||||
final int teBlockId = resolveTeBlockId(teMap, renderBlocks.blockAccess, block, metadata, x, y, z, buffers, pass, materialOverride, currentTick);
|
||||
final ContextAwareChunkVertexEncoder contextEncoder = prepareEncode(block, metadata, x, y, z, renderBlocks.blockAccess, buffers, blockRenderContext, passMaterial, teBlockId);
|
||||
drawBlock(block, x, y, z, pass, tessellator, renderBlocks);
|
||||
encodeTessellated(tessellator.rawBuffer, 0, tessellator.vertexCount, ((StateAwareTessellator) tessellator).angelica$getVertexStates(),
|
||||
((StateAwareTessellator) tessellator).angelica$getShaderOverrideBlockIds(), block, buffers, buildContext, passMaterial, isShaderPackOverride, contextEncoder);
|
||||
tessellator.reset();
|
||||
tessellator.isDrawing = false;
|
||||
}
|
||||
|
||||
private static Material resolvePassMaterial(Block block, int pass, ChunkBuildBuffers buffers, Material materialOverride) {
|
||||
// Use material override if provided, otherwise derive from pass
|
||||
// Lava is opaque - use solid to skip unnecessary alpha testing
|
||||
if (materialOverride != null) {
|
||||
return materialOverride;
|
||||
} else if (block.getMaterial() == net.minecraft.block.material.Material.lava) {
|
||||
return AngelicaRenderPassConfiguration.SOLID_MATERIAL;
|
||||
} else {
|
||||
return buffers.getRenderPassConfiguration().getMaterialForRenderType(BlockRenderLayer.fromVanillaPass(pass));
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isFluid(Block block) {
|
||||
final var blockMaterial = block.getMaterial();
|
||||
return blockMaterial == net.minecraft.block.material.Material.water || blockMaterial == net.minecraft.block.material.Material.lava;
|
||||
}
|
||||
|
||||
private static ContextAwareChunkVertexEncoder prepareEncode(Block block, int metadata, int x, int y, int z, IBlockAccess blockAccess, ChunkBuildBuffers buffers, BlockRenderContext blockRenderContext, Material passMaterial, int teBlockId) {
|
||||
final var encoder = buffers.get(passMaterial).getEncoder();
|
||||
final ContextAwareChunkVertexEncoder contextEncoder = (encoder instanceof ContextAwareChunkVertexEncoder) ? (ContextAwareChunkVertexEncoder) encoder : null;
|
||||
|
||||
@@ -469,43 +535,49 @@ public abstract class AngelicaChunkBuilderMeshingTask extends ChunkBuilderTask<C
|
||||
|
||||
if (contextEncoder != null) {
|
||||
final byte lightValue = (byte) block.getLightValue();
|
||||
if (isFluid) {
|
||||
if (isFluid(block)) {
|
||||
contextEncoder.prepareToRenderFluid(blockRenderContext, block, metadata, lightValue);
|
||||
} else {
|
||||
int effectiveMeta = metadata;
|
||||
if (BlockRenderingSettings.INSTANCE.hasSnowyEntries()
|
||||
&& BlockRenderingSettings.INSTANCE.getSnowyBlocks().contains(block)
|
||||
&& RenderBlocksUtils.isSnowCovered(renderBlocks.blockAccess, x, y, z)) {
|
||||
&& RenderBlocksUtils.isSnowCovered(blockAccess, x, y, z)) {
|
||||
effectiveMeta |= BlockMaterialMapping.SNOWY_META_BIT;
|
||||
} else if (block instanceof BlockDoublePlant doublePlant && BlockDoublePlant.func_149887_c(metadata)) {
|
||||
effectiveMeta = 0x8 | (doublePlant.func_149885_e(renderBlocks.blockAccess, x, y, z) & 7);
|
||||
effectiveMeta = 0x8 | (doublePlant.func_149885_e(blockAccess, x, y, z) & 7);
|
||||
}
|
||||
contextEncoder.prepareToRenderBlock(blockRenderContext, block, effectiveMeta,
|
||||
ExtendedDataHelper.BLOCK_RENDER_TYPE, lightValue);
|
||||
// Check for TileEntity NBT-conditional shader block ID override
|
||||
if (teMap != null && teMap.hasConditions(block)) {
|
||||
final long packedPos = BlockRenderingSettings.packBlockPos(x, y, z);
|
||||
int teBlockId = BlockRenderingSettings.getCachedTeNbtId(packedPos, currentTick);
|
||||
|
||||
if (teBlockId == BlockRenderingSettings.CACHE_MISS) {
|
||||
final TileEntity te = renderBlocks.blockAccess.getTileEntity(x, y, z);
|
||||
if (te != null) {
|
||||
final NBTTagCompound teNbt = new NBTTagCompound();
|
||||
te.writeToNBT(teNbt);
|
||||
teBlockId = teMap.resolve(block, teNbt, metadata);
|
||||
} else {
|
||||
teBlockId = -1;
|
||||
}
|
||||
BlockRenderingSettings.cacheTeNbtId(packedPos, teBlockId, currentTick);
|
||||
}
|
||||
|
||||
if (teBlockId != -1) {
|
||||
blockRenderContext.blockId = (short) teBlockId;
|
||||
}
|
||||
if (teBlockId != -1) {
|
||||
blockRenderContext.blockId = (short) teBlockId;
|
||||
}
|
||||
}
|
||||
}
|
||||
return contextEncoder;
|
||||
}
|
||||
|
||||
private static int resolveTeBlockId(NbtConditionalIdMap<Block> teMap, IBlockAccess blockAccess, Block block, int metadata, int x, int y, int z, ChunkBuildBuffers buffers, int pass, Material materialOverride, long currentTick) {
|
||||
// Check for TileEntity NBT-conditional shader block ID override
|
||||
if (teMap == null || !teMap.hasConditions(block) || isFluid(block)) return -1;
|
||||
if (!(buffers.get(resolvePassMaterial(block, pass, buffers, materialOverride)).getEncoder() instanceof ContextAwareChunkVertexEncoder)) return -1;
|
||||
final long packedPos = BlockRenderingSettings.packBlockPos(x, y, z);
|
||||
int teBlockId = BlockRenderingSettings.getCachedTeNbtId(packedPos, currentTick);
|
||||
|
||||
if (teBlockId == BlockRenderingSettings.CACHE_MISS) {
|
||||
final TileEntity te = blockAccess.getTileEntity(x, y, z);
|
||||
if (te != null) {
|
||||
final NBTTagCompound teNbt = new NBTTagCompound();
|
||||
te.writeToNBT(teNbt);
|
||||
teBlockId = teMap.resolve(block, teNbt, metadata);
|
||||
} else {
|
||||
teBlockId = -1;
|
||||
}
|
||||
BlockRenderingSettings.cacheTeNbtId(packedPos, teBlockId, currentTick);
|
||||
}
|
||||
return teBlockId;
|
||||
}
|
||||
|
||||
private void drawBlock(Block block, int x, int y, int z, int pass, Tessellator tessellator, RenderBlocks renderBlocks) {
|
||||
setRenderPass(pass);
|
||||
// Trigger side effects from canRenderInPass (some ISBRHs like BuildCraft set global state in this method that gets read elsewhere renderWorldBlock)
|
||||
block.canRenderInPass(pass);
|
||||
@@ -518,15 +590,13 @@ public abstract class AngelicaChunkBuilderMeshingTask extends ChunkBuilderTask<C
|
||||
renderBlocks.renderBlockByRenderType(block, x, y, z);
|
||||
|
||||
if (applyNaturalTex) { renderBlocks.uvRotateTop = 0; } // Reset
|
||||
}
|
||||
|
||||
private static void encodeTessellated(int[] rawBuffer, int vertexOffset, int vertexCount, int[] vertexStates, int[] shaderOverrideBlockIds, Block block, ChunkBuildBuffers buffers, AngelicaChunkBuildContext buildContext, Material passMaterial, boolean isShaderPackOverride, ContextAwareChunkVertexEncoder contextEncoder) {
|
||||
boolean blockAllowsSmoothLighting = Minecraft.isAmbientOcclusionEnabled() // smooth lighting on
|
||||
&& block.getLightValue() == 0; // does not emit real block light
|
||||
buildContext.copyRawBuffer(tessellator.rawBuffer, tessellator.vertexCount,
|
||||
((StateAwareTessellator)tessellator).angelica$getVertexStates(),
|
||||
((StateAwareTessellator)tessellator).angelica$getShaderOverrideBlockIds(),
|
||||
buildContext.copyRawBuffer(rawBuffer, vertexOffset, vertexCount, vertexStates, shaderOverrideBlockIds,
|
||||
buffers, passMaterial, isShaderPackOverride, blockAllowsSmoothLighting);
|
||||
tessellator.reset();
|
||||
tessellator.isDrawing = false;
|
||||
|
||||
if (contextEncoder != null) {
|
||||
contextEncoder.finishRenderingBlock();
|
||||
|
||||
+4
-1
@@ -55,6 +55,7 @@ public class AngelicaRenderSectionManager extends RenderSectionManager {
|
||||
private static final long P_DRAW_REGION_BUFFERS = Tracy.plotHandle("draw.regionBuffers");
|
||||
private static final long P_MESH_DEVICE_ALLOCATED = Tracy.plotHandle("mesh.deviceAllocated", TracyBackend.PLOT_FORMAT_MEMORY);
|
||||
private static final long P_MESH_DEFERRED_PENDING = Tracy.plotHandle("mesh.deferredPending");
|
||||
private static final long P_MESH_DEFERRED_FINISHING = Tracy.plotHandle("mesh.deferredFinishing");
|
||||
|
||||
private static final Tracy.ZoneId Z_BIOME_REBUILDS = Tracy.zoneId("biomeRebuilds", Tracy.COLOR_TERRAIN);
|
||||
private static final Tracy.ZoneId Z_GRAPH_SEARCH = Tracy.zoneId("graphSearch", Tracy.COLOR_TERRAIN);
|
||||
@@ -72,7 +73,7 @@ public class AngelicaRenderSectionManager extends RenderSectionManager {
|
||||
this.world = world;
|
||||
this.sectionCache = new ClonedChunkSectionCache(world);
|
||||
this.taskProvider = taskProvider;
|
||||
this.deferredScheduler = new DeferredMeshScheduler(configuration, () -> new AngelicaChunkBuildContext(configuration, world), () -> new WorldSlice(world), AngelicaRenderQueue::submit, ((RenderSectionManagerAccessor) this).angelica$getBuildResults());
|
||||
this.deferredScheduler = new DeferredMeshScheduler(configuration, getBuilder(), () -> new WorldSlice(world), AngelicaRenderQueue::submit, ((RenderSectionManagerAccessor) this).angelica$getBuildResults());
|
||||
}
|
||||
|
||||
public static AngelicaRenderSectionManager create(ChunkVertexType vertexType, WorldClient world, int renderDistance, CommandList commandList) {
|
||||
@@ -225,6 +226,7 @@ public class AngelicaRenderSectionManager extends RenderSectionManager {
|
||||
this.deferredScheduler.markDestroyed();
|
||||
super.destroy();
|
||||
this.deferredScheduler.runImportant();
|
||||
this.deferredScheduler.releaseOrphans();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -350,5 +352,6 @@ public class AngelicaRenderSectionManager extends RenderSectionManager {
|
||||
Tracy.plotInt(P_MESH_DEVICE_ALLOCATED, mem.deviceAllocated + mem.indexAllocated);
|
||||
Tracy.plotInt(P_DRAW_REGION_BUFFERS, mem.bufferCount);
|
||||
Tracy.plotInt(P_MESH_DEFERRED_PENDING, this.deferredScheduler.pendingCount());
|
||||
Tracy.plotInt(P_MESH_DEFERRED_FINISHING, this.deferredScheduler.finishingCount());
|
||||
}
|
||||
}
|
||||
@@ -3,4 +3,8 @@ package com.gtnewhorizons.angelica.rendering.celeritas;
|
||||
import net.minecraft.block.Block;
|
||||
import org.embeddedt.embeddium.impl.render.chunk.terrain.material.Material;
|
||||
|
||||
public record DeferredBlock(int x, int y, int z, Block block, int meta, int pass, Material materialOverride, boolean isShaderPackOverride) {}
|
||||
public record DeferredBlock(int x, int y, int z, Block block, int meta, int passMask, Material materialOverride) {
|
||||
public boolean isShaderPackOverride() {
|
||||
return materialOverride != null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.gtnewhorizons.angelica.rendering.celeritas;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
|
||||
import org.embeddedt.embeddium.impl.render.chunk.compile.ChunkBuildContext;
|
||||
import org.embeddedt.embeddium.impl.render.chunk.compile.ChunkBuildOutput;
|
||||
import org.embeddedt.embeddium.impl.render.chunk.compile.executor.ChunkJobCollector;
|
||||
import org.embeddedt.embeddium.impl.render.chunk.compile.executor.ChunkJobResult;
|
||||
import org.embeddedt.embeddium.impl.render.chunk.compile.tasks.ChunkBuilderTask;
|
||||
import org.embeddedt.embeddium.impl.util.task.CancellationToken;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public final class DeferredFinishTask extends ChunkBuilderTask<ChunkBuildOutput> implements Consumer<ChunkJobResult<ChunkBuildOutput>> {
|
||||
private static final Tracy.ZoneId Z_MESH_DEFERRED_FINISH = Tracy.zoneId("meshDeferredFinish");
|
||||
|
||||
private final DeferredSectionMesh mesh;
|
||||
private final DeferredMeshScheduler scheduler;
|
||||
private final @Nullable ChunkJobCollector collector;
|
||||
|
||||
public DeferredFinishTask(DeferredSectionMesh mesh, DeferredMeshScheduler scheduler, @Nullable ChunkJobCollector collector) {
|
||||
this.mesh = mesh;
|
||||
this.scheduler = scheduler;
|
||||
this.collector = collector;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChunkBuildOutput execute(ChunkBuildContext context, CancellationToken cancellationToken) {
|
||||
if (mesh.isObsolete()) return null;
|
||||
if (Tracy.ENABLED) Tracy.beginZone(Z_MESH_DEFERRED_FINISH);
|
||||
try {
|
||||
return mesh.finish(context);
|
||||
} finally {
|
||||
if (Tracy.ENABLED) Tracy.endZone();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(@Nullable ChunkJobResult<ChunkBuildOutput> result) {
|
||||
scheduler.finished(mesh, result, collector);
|
||||
}
|
||||
}
|
||||
+92
-22
@@ -2,37 +2,56 @@ package com.gtnewhorizons.angelica.rendering.celeritas;
|
||||
|
||||
import org.embeddedt.embeddium.impl.render.chunk.RenderPassConfiguration;
|
||||
import org.embeddedt.embeddium.impl.render.chunk.compile.ChunkBuildBuffers;
|
||||
import org.embeddedt.embeddium.impl.render.chunk.compile.ChunkBuildOutput;
|
||||
import org.embeddedt.embeddium.impl.render.chunk.compile.ChunkTaskOutput;
|
||||
import org.embeddedt.embeddium.impl.render.chunk.compile.executor.ChunkBuilder;
|
||||
import org.embeddedt.embeddium.impl.render.chunk.compile.executor.ChunkJobCollector;
|
||||
import org.embeddedt.embeddium.impl.render.chunk.compile.executor.ChunkJobResult;
|
||||
import org.embeddedt.embeddium.impl.render.chunk.compile.executor.ChunkJobTyped;
|
||||
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.DeferredSectionMesh.DeferredCapture;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.world.WorldSlice;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.PriorityBlockingQueue;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class DeferredMeshScheduler {
|
||||
private final Supplier<AngelicaChunkBuildContext> mainContextFactory;
|
||||
/** Must stay below every distance priority: queued finishes hold pooled buffers and slices. */
|
||||
private static final long FINISH_PRIORITY = -1L;
|
||||
|
||||
private static final Comparator<DeferredSectionMesh> NEAREST_FIRST = Comparator.comparingLong(m -> m.priority);
|
||||
|
||||
private final ChunkBuilder builder;
|
||||
private final Consumer<Runnable> mainThreadQueue;
|
||||
private final Queue<ChunkJobResult<? extends ChunkTaskOutput>> results;
|
||||
private final Consumer<ChunkJobResult<? extends ChunkTaskOutput>> resultSink;
|
||||
private final ObjectPool<ChunkBuildBuffers> buffers;
|
||||
private final ObjectPool<WorldSlice> slices;
|
||||
private final ObjectPool<DeferredCapture> captures;
|
||||
private final ConcurrentLinkedQueue<DeferredSectionMesh> important = new ConcurrentLinkedQueue<>();
|
||||
private final ConcurrentLinkedQueue<DeferredSectionMesh> normal = new ConcurrentLinkedQueue<>();
|
||||
private final PriorityBlockingQueue<DeferredSectionMesh> normal = new PriorityBlockingQueue<>(11, NEAREST_FIRST);
|
||||
private final Set<DeferredSectionMesh> inFlight = ConcurrentHashMap.newKeySet();
|
||||
private final Runnable runNext = this::runNext;
|
||||
private final AtomicInteger pending = new AtomicInteger();
|
||||
|
||||
private AngelicaChunkBuildContext mainContext;
|
||||
private boolean destroyed;
|
||||
|
||||
public DeferredMeshScheduler(RenderPassConfiguration<?> config, Supplier<AngelicaChunkBuildContext> mainContextFactory, Supplier<WorldSlice> sliceFactory, Consumer<Runnable> mainThreadQueue, Queue<ChunkJobResult<? extends ChunkTaskOutput>> results) {
|
||||
this.mainContextFactory = mainContextFactory;
|
||||
public DeferredMeshScheduler(RenderPassConfiguration<?> config, ChunkBuilder builder, Supplier<WorldSlice> sliceFactory, Consumer<Runnable> mainThreadQueue, Queue<ChunkJobResult<? extends ChunkTaskOutput>> results) {
|
||||
this.builder = builder;
|
||||
this.mainThreadQueue = mainThreadQueue;
|
||||
this.results = results;
|
||||
this.buffers = new ObjectPool<>(() -> new ChunkBuildBuffers(config));
|
||||
this.slices = new ObjectPool<>(sliceFactory);
|
||||
this.resultSink = results::add;
|
||||
this.buffers = new ObjectPool<>(() -> new ChunkBuildBuffers(config), Integer.MAX_VALUE);
|
||||
this.slices = new ObjectPool<>(sliceFactory, Integer.MAX_VALUE);
|
||||
this.captures = new ObjectPool<>(DeferredCapture::new, Math.max(1, builder.getTotalThreadCount()) * 2);
|
||||
}
|
||||
|
||||
public ChunkBuildBuffers acquireBuffers() {
|
||||
@@ -59,17 +78,30 @@ public class DeferredMeshScheduler {
|
||||
}
|
||||
|
||||
public void runImportant() {
|
||||
if (important.isEmpty()) return;
|
||||
final ChunkJobCollector collector = new ChunkJobCollector(Integer.MAX_VALUE, resultSink);
|
||||
DeferredSectionMesh m;
|
||||
while ((m = important.poll()) != null) {
|
||||
run(m);
|
||||
run(m, collector);
|
||||
}
|
||||
collector.awaitCompletion(builder);
|
||||
}
|
||||
|
||||
public void markDestroyed() {
|
||||
destroyed = true;
|
||||
if (mainContext != null) {
|
||||
mainContext.cleanup();
|
||||
mainContext = null;
|
||||
}
|
||||
|
||||
/** Call after the builder shut down: its queued jobs were dropped without reaching their consumers. */
|
||||
public void releaseOrphans() {
|
||||
DeferredSectionMesh m;
|
||||
while ((m = important.poll()) != null) {
|
||||
release(m);
|
||||
}
|
||||
while ((m = normal.poll()) != null) {
|
||||
release(m);
|
||||
}
|
||||
for (final DeferredSectionMesh orphan : inFlight) {
|
||||
if (inFlight.remove(orphan)) release(orphan);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,29 +109,67 @@ public class DeferredMeshScheduler {
|
||||
return pending.get();
|
||||
}
|
||||
|
||||
private AngelicaChunkBuildContext mainContext() {
|
||||
if (mainContext == null) {
|
||||
mainContext = mainContextFactory.get();
|
||||
public int finishingCount() {
|
||||
return inFlight.size();
|
||||
}
|
||||
|
||||
void finished(DeferredSectionMesh m, @Nullable ChunkJobResult<ChunkBuildOutput> result, @Nullable ChunkJobCollector collector) {
|
||||
final long nanos = m.workerNanos + m.mainNanos;
|
||||
ChunkJobResult<ChunkBuildOutput> out = result instanceof ChunkJobResult.Success<ChunkBuildOutput> success ? new ChunkJobResult.Success<>(success.output(), nanos + success.executionTimeNanos()) : result;
|
||||
if (inFlight.remove(m)) {
|
||||
// Runs as a celeritas job consumer on a worker, where an escaping throwable kills the worker thread.
|
||||
try {
|
||||
release(m);
|
||||
} catch (Throwable t) {
|
||||
out = new ChunkJobResult.Failure<>(t);
|
||||
}
|
||||
}
|
||||
if (collector != null) {
|
||||
collector.onJobFinished(out);
|
||||
} else if (out != null) {
|
||||
results.add(out);
|
||||
}
|
||||
return mainContext;
|
||||
}
|
||||
|
||||
private void runNext() {
|
||||
final DeferredSectionMesh m = normal.poll();
|
||||
if (m != null) {
|
||||
run(m);
|
||||
run(m, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void run(DeferredSectionMesh m) {
|
||||
private void run(DeferredSectionMesh m, @Nullable ChunkJobCollector collector) {
|
||||
boolean scheduled = false;
|
||||
try {
|
||||
if (destroyed || m.isObsolete()) return;
|
||||
results.add(m.complete(mainContext()));
|
||||
m.capture = captures.acquire();
|
||||
final ChunkJobResult<ChunkBuildOutput> failure = m.capture();
|
||||
if (failure != null) {
|
||||
results.add(failure);
|
||||
return;
|
||||
}
|
||||
final DeferredFinishTask task = new DeferredFinishTask(m, this, collector);
|
||||
inFlight.add(m);
|
||||
final ChunkJobTyped<DeferredFinishTask, ChunkBuildOutput> job = builder.scheduleTask(task, m.important, FINISH_PRIORITY, task);
|
||||
scheduled = true;
|
||||
if (collector != null) collector.addSubmittedJob(job);
|
||||
} finally {
|
||||
releaseBuffers(m.buffers);
|
||||
m.slice.releaseContext();
|
||||
slices.release(m.slice);
|
||||
pending.decrementAndGet();
|
||||
if (!scheduled) {
|
||||
inFlight.remove(m);
|
||||
release(m);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void release(DeferredSectionMesh m) {
|
||||
releaseBuffers(m.buffers);
|
||||
m.slice.releaseContext();
|
||||
slices.release(m.slice);
|
||||
if (m.capture != null) {
|
||||
m.capture.clear();
|
||||
captures.release(m.capture);
|
||||
m.capture = null;
|
||||
}
|
||||
pending.decrementAndGet();
|
||||
}
|
||||
}
|
||||
+59
-3
@@ -1,9 +1,11 @@
|
||||
package com.gtnewhorizons.angelica.rendering.celeritas;
|
||||
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.world.WorldSlice;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
import net.coderbot.iris.block_rendering.NbtConditionalIdMap;
|
||||
import net.minecraft.block.Block;
|
||||
import org.embeddedt.embeddium.impl.render.chunk.compile.ChunkBuildBuffers;
|
||||
import org.embeddedt.embeddium.impl.render.chunk.compile.ChunkBuildContext;
|
||||
import org.embeddedt.embeddium.impl.render.chunk.compile.ChunkBuildOutput;
|
||||
import org.embeddedt.embeddium.impl.render.chunk.compile.executor.ChunkJobResult;
|
||||
import org.embeddedt.embeddium.impl.util.task.CancellationToken;
|
||||
@@ -20,9 +22,12 @@ public class DeferredSectionMesh {
|
||||
public final NbtConditionalIdMap<Block> teMap;
|
||||
public final long currentTick;
|
||||
public final boolean important;
|
||||
public final long priority;
|
||||
public final long workerNanos;
|
||||
public DeferredCapture capture;
|
||||
public long mainNanos;
|
||||
|
||||
public DeferredSectionMesh(AngelicaChunkBuilderMeshingTask task, CancellationToken token, ChunkBuildBuffers buffers, WorldSlice slice, AngelicaBuiltRenderSectionData renderData, List<DeferredBlock> blocks, NbtConditionalIdMap<Block> teMap, long currentTick, boolean important, long workerNanos) {
|
||||
public DeferredSectionMesh(AngelicaChunkBuilderMeshingTask task, CancellationToken token, ChunkBuildBuffers buffers, WorldSlice slice, AngelicaBuiltRenderSectionData renderData, List<DeferredBlock> blocks, NbtConditionalIdMap<Block> teMap, long currentTick, boolean important, long priority, long workerNanos) {
|
||||
this.task = task;
|
||||
this.token = token;
|
||||
this.buffers = buffers;
|
||||
@@ -32,6 +37,7 @@ public class DeferredSectionMesh {
|
||||
this.teMap = teMap;
|
||||
this.currentTick = currentTick;
|
||||
this.important = important;
|
||||
this.priority = priority;
|
||||
this.workerNanos = workerNanos;
|
||||
}
|
||||
|
||||
@@ -39,7 +45,57 @@ public class DeferredSectionMesh {
|
||||
return token.isCancelled() || task.render.isDisposed() || task.render.getLastBuiltFrame() > task.buildTime;
|
||||
}
|
||||
|
||||
public ChunkJobResult<ChunkBuildOutput> complete(AngelicaChunkBuildContext ctx) {
|
||||
return task.completeDeferred(this, ctx);
|
||||
/** Main thread only; returns a failure or null. */
|
||||
public ChunkJobResult<ChunkBuildOutput> capture() {
|
||||
return task.captureDeferred(this);
|
||||
}
|
||||
|
||||
public ChunkBuildOutput finish(ChunkBuildContext context) {
|
||||
return task.finishDeferred(this, (AngelicaChunkBuildContext) context);
|
||||
}
|
||||
|
||||
public static final class DeferredCapture {
|
||||
public static final int VERTEX_TRIM = 1 << 17;
|
||||
|
||||
public final IntArrayList raw = new IntArrayList();
|
||||
public final IntArrayList vertexStates = new IntArrayList();
|
||||
public final IntArrayList shaderBlockIds = new IntArrayList();
|
||||
public final IntArrayList blockIndex = new IntArrayList();
|
||||
public final IntArrayList blockStart = new IntArrayList();
|
||||
public final IntArrayList blockVertexCount = new IntArrayList();
|
||||
public final IntArrayList teBlockIds = new IntArrayList();
|
||||
|
||||
public void add(int blockIdx, int[] rawBuffer, int vertexCount, int[] states, int[] shaderIds, int teBlockId) {
|
||||
blockIndex.add(blockIdx);
|
||||
blockStart.add(vertexStates.size());
|
||||
blockVertexCount.add(vertexCount);
|
||||
teBlockIds.add(teBlockId);
|
||||
raw.addElements(raw.size(), rawBuffer, 0, vertexCount * AngelicaChunkBuildContext.INTS_PER_VERTEX);
|
||||
vertexStates.addElements(vertexStates.size(), states, 0, vertexCount);
|
||||
shaderBlockIds.addElements(shaderBlockIds.size(), shaderIds, 0, vertexCount);
|
||||
}
|
||||
|
||||
public int blockCount() {
|
||||
return blockIndex.size();
|
||||
}
|
||||
|
||||
public int vertexCount() {
|
||||
return vertexStates.size();
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
reset(raw, VERTEX_TRIM * AngelicaChunkBuildContext.INTS_PER_VERTEX);
|
||||
reset(vertexStates, VERTEX_TRIM);
|
||||
reset(shaderBlockIds, VERTEX_TRIM);
|
||||
reset(blockIndex, VERTEX_TRIM);
|
||||
reset(blockStart, VERTEX_TRIM);
|
||||
reset(blockVertexCount, VERTEX_TRIM);
|
||||
reset(teBlockIds, VERTEX_TRIM);
|
||||
}
|
||||
|
||||
private static void reset(IntArrayList list, int cap) {
|
||||
list.clear();
|
||||
if (list.elements().length > cap) list.trim(cap);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,32 @@
|
||||
package com.gtnewhorizons.angelica.rendering.celeritas;
|
||||
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
final class ObjectPool<T> {
|
||||
private final ConcurrentLinkedQueue<T> free = new ConcurrentLinkedQueue<>();
|
||||
private final ConcurrentLinkedDeque<T> free = new ConcurrentLinkedDeque<>();
|
||||
private final AtomicInteger freeCount = new AtomicInteger();
|
||||
private final Supplier<T> factory;
|
||||
private final int maxFree;
|
||||
|
||||
ObjectPool(Supplier<T> factory) {
|
||||
ObjectPool(Supplier<T> factory, int maxFree) {
|
||||
this.factory = factory;
|
||||
this.maxFree = maxFree;
|
||||
}
|
||||
|
||||
T acquire() {
|
||||
final T t = free.poll();
|
||||
return t != null ? t : factory.get();
|
||||
final T t = free.pollFirst();
|
||||
if (t == null) return factory.get();
|
||||
freeCount.decrementAndGet();
|
||||
return t;
|
||||
}
|
||||
|
||||
void release(T t) {
|
||||
free.add(t);
|
||||
if (freeCount.incrementAndGet() > maxFree) {
|
||||
freeCount.decrementAndGet();
|
||||
return;
|
||||
}
|
||||
free.offerFirst(t);
|
||||
}
|
||||
}
|
||||
+162
-114
@@ -3,8 +3,10 @@ package com.gtnewhorizons.angelica.rendering.celeritas;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.world.WorldSlice;
|
||||
|
||||
import org.embeddedt.embeddium.impl.render.chunk.compile.ChunkBuildBuffers;
|
||||
import org.embeddedt.embeddium.impl.render.chunk.compile.ChunkBuildContext;
|
||||
import org.embeddedt.embeddium.impl.render.chunk.compile.ChunkBuildOutput;
|
||||
import org.embeddedt.embeddium.impl.render.chunk.compile.ChunkTaskOutput;
|
||||
import org.embeddedt.embeddium.impl.render.chunk.compile.executor.ChunkBuilder;
|
||||
import org.embeddedt.embeddium.impl.render.chunk.compile.executor.ChunkJobResult;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
@@ -13,160 +15,206 @@ import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class DeferredMeshSchedulerTest {
|
||||
|
||||
private static DeferredSectionMesh mesh(ChunkBuildBuffers buffers, WorldSlice slice, boolean important, boolean obsolete, ChunkJobResult<ChunkBuildOutput> result) {
|
||||
return new DeferredSectionMesh(null, null, buffers, slice, null, null, null, 0L, important, 0L) {
|
||||
private static final int NO_WORKERS = -1;
|
||||
|
||||
private record Row(String label, boolean important, boolean obsoleteAtCapture, boolean obsoleteAtFinish, boolean captureFails, boolean finishThrows, boolean destroyed, int expectedResults, Class<?> expectedClass) {
|
||||
@Override
|
||||
public String toString() {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Probe {
|
||||
final AtomicInteger obsoleteChecks = new AtomicInteger();
|
||||
final AtomicReference<Thread> finishThread = new AtomicReference<>();
|
||||
final List<Long> captureOrder = new ArrayList<>();
|
||||
final List<Integer> blocksOnCapture = new ArrayList<>();
|
||||
}
|
||||
|
||||
private static DeferredSectionMesh mesh(Probe probe, ChunkBuildBuffers buffers, WorldSlice slice, boolean important, long priority, boolean obsoleteAtCapture, boolean obsoleteAtFinish, boolean captureFails, boolean finishThrows) {
|
||||
return new DeferredSectionMesh(null, null, buffers, slice, null, null, null, 0L, important, priority, 0L) {
|
||||
@Override
|
||||
public boolean isObsolete() {
|
||||
return obsolete;
|
||||
return probe.obsoleteChecks.getAndIncrement() == 0 ? obsoleteAtCapture : obsoleteAtFinish;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChunkJobResult<ChunkBuildOutput> complete(AngelicaChunkBuildContext ctx) {
|
||||
return result;
|
||||
public ChunkJobResult<ChunkBuildOutput> capture() {
|
||||
probe.captureOrder.add(priority);
|
||||
probe.blocksOnCapture.add(capture.blockCount());
|
||||
capture.add(0, new int[8 * 4], 4, new int[4], new int[4], 1);
|
||||
return captureFails ? new ChunkJobResult.Failure<>(new RuntimeException("capture")) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChunkBuildOutput finish(ChunkBuildContext context) {
|
||||
probe.finishThread.set(Thread.currentThread());
|
||||
if (finishThrows) throw new IllegalStateException("finish");
|
||||
return Mockito.mock(ChunkBuildOutput.class);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static ChunkJobResult<ChunkBuildOutput> success() {
|
||||
return new ChunkJobResult.Success<>(null, 0L);
|
||||
}
|
||||
|
||||
private static ChunkJobResult<ChunkBuildOutput> failure() {
|
||||
return new ChunkJobResult.Failure<>(new RuntimeException("x"));
|
||||
}
|
||||
|
||||
private static final class Harness {
|
||||
final ArrayDeque<Runnable> queue = new ArrayDeque<>();
|
||||
final Queue<ChunkJobResult<? extends ChunkTaskOutput>> results = new ConcurrentLinkedDeque<>();
|
||||
final DeferredMeshScheduler scheduler = new DeferredMeshScheduler(null, () -> null, () -> Mockito.mock(WorldSlice.class), queue::add, results);
|
||||
final Probe probe = new Probe();
|
||||
final AtomicInteger slicesCreated = new AtomicInteger();
|
||||
final ChunkBuilder builder;
|
||||
final DeferredMeshScheduler scheduler;
|
||||
|
||||
Harness(int threads) {
|
||||
builder = new ChunkBuilder(ChunkBuilder.ManagedBlocker.NONE, () -> new ChunkBuildContext(null), threads);
|
||||
scheduler = new DeferredMeshScheduler(null, builder, () -> {
|
||||
slicesCreated.incrementAndGet();
|
||||
return Mockito.mock(WorldSlice.class);
|
||||
}, queue::add, results);
|
||||
}
|
||||
|
||||
DeferredSectionMesh submit(boolean important, long priority) {
|
||||
final DeferredSectionMesh m = mesh(probe, scheduler.acquireBuffers(), scheduler.acquireSlice(), important, priority, false, false, false, false);
|
||||
scheduler.submit(m);
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void importantItemRunsOnRunImportant() {
|
||||
Harness h = new Harness();
|
||||
|
||||
ChunkBuildBuffers buffers = h.scheduler.acquireBuffers();
|
||||
DeferredSectionMesh m = mesh(buffers, h.scheduler.acquireSlice(), true, false, success());
|
||||
h.scheduler.submit(m);
|
||||
|
||||
assertEquals(0, h.queue.size());
|
||||
assertEquals(1, h.scheduler.pendingCount());
|
||||
|
||||
h.scheduler.runImportant();
|
||||
|
||||
assertEquals(1, h.results.size());
|
||||
assertTrue(h.results.peek() instanceof ChunkJobResult.Success);
|
||||
assertEquals(0, h.scheduler.pendingCount());
|
||||
assertSame(buffers, h.scheduler.acquireBuffers());
|
||||
private static Stream<Arguments> rows() {
|
||||
return Stream.of(
|
||||
Arguments.of(new Row("normal", false, false, false, false, false, false, 1, ChunkJobResult.Success.class)),
|
||||
Arguments.of(new Row("important", true, false, false, false, false, false, 1, ChunkJobResult.Success.class)),
|
||||
Arguments.of(new Row("capture fails", false, false, false, true, false, false, 1, ChunkJobResult.Failure.class)),
|
||||
Arguments.of(new Row("normal finish throws", false, false, false, false, true, false, 1, ChunkJobResult.Failure.class)),
|
||||
Arguments.of(new Row("important finish throws", true, false, false, false, true, false, 1, ChunkJobResult.Failure.class)),
|
||||
Arguments.of(new Row("obsolete at capture", false, true, false, false, false, false, 0, null)),
|
||||
Arguments.of(new Row("normal obsolete at finish", false, false, true, false, false, false, 0, null)),
|
||||
Arguments.of(new Row("important obsolete at finish", true, false, true, false, false, false, 0, null)),
|
||||
Arguments.of(new Row("normal destroyed", false, false, false, false, false, true, 0, null)),
|
||||
Arguments.of(new Row("important destroyed", true, false, false, false, false, true, 0, null)));
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{0}")
|
||||
@MethodSource("normalResults")
|
||||
void normalItemRunsThroughMainThreadQueue(String label, Supplier<ChunkJobResult<ChunkBuildOutput>> result, Class<?> expected) {
|
||||
Harness h = new Harness();
|
||||
@MethodSource("rows")
|
||||
void submittedMeshFinishesAndReleasesEverything(Row row) {
|
||||
Harness h = new Harness(NO_WORKERS);
|
||||
|
||||
ChunkBuildBuffers buffers = h.scheduler.acquireBuffers();
|
||||
DeferredSectionMesh m = mesh(buffers, h.scheduler.acquireSlice(), false, false, result.get());
|
||||
WorldSlice slice = h.scheduler.acquireSlice();
|
||||
DeferredSectionMesh m = mesh(h.probe, buffers, slice, row.important, 0L, row.obsoleteAtCapture, row.obsoleteAtFinish, row.captureFails, row.finishThrows);
|
||||
h.scheduler.submit(m);
|
||||
if (row.destroyed) h.scheduler.markDestroyed();
|
||||
|
||||
assertEquals(1, h.queue.size());
|
||||
assertEquals(row.important ? 0 : 1, h.queue.size());
|
||||
|
||||
if (row.important) {
|
||||
h.scheduler.runImportant();
|
||||
} else {
|
||||
h.queue.poll().run();
|
||||
assertEquals(row.captureFails ? 1 : 0, h.results.size());
|
||||
h.builder.tick();
|
||||
}
|
||||
|
||||
assertEquals(row.expectedResults, h.results.size());
|
||||
if (row.expectedClass != null) assertTrue(row.expectedClass.isInstance(h.results.peek()));
|
||||
assertEquals(0, h.scheduler.pendingCount());
|
||||
assertEquals(0, h.scheduler.finishingCount());
|
||||
assertNull(m.capture);
|
||||
assertSame(buffers, h.scheduler.acquireBuffers());
|
||||
assertSame(slice, h.scheduler.acquireSlice());
|
||||
Mockito.verify(slice, Mockito.times(1)).releaseContext();
|
||||
boolean finishRan = !row.captureFails && !row.obsoleteAtCapture && !row.destroyed && !row.obsoleteAtFinish;
|
||||
assertEquals(finishRan ? Thread.currentThread() : null, h.probe.finishThread.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void finishRunsOnWorkerThread() throws InterruptedException {
|
||||
Harness h = new Harness(1);
|
||||
try {
|
||||
h.submit(false, 0L);
|
||||
h.queue.poll().run();
|
||||
|
||||
long deadline = System.nanoTime() + 10_000_000_000L;
|
||||
while (h.results.isEmpty() && System.nanoTime() < deadline) Thread.sleep(5);
|
||||
|
||||
assertTrue(h.results.peek() instanceof ChunkJobResult.Success);
|
||||
assertTrue(h.probe.finishThread.get() instanceof ChunkBuilder.WorkerThread);
|
||||
assertEquals(0, h.scheduler.pendingCount());
|
||||
} finally {
|
||||
h.builder.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void normalQueueDrainsNearestFirst() {
|
||||
Harness h = new Harness(NO_WORKERS);
|
||||
|
||||
for (long p : new long[] {30L, 10L, 20L}) h.submit(false, p);
|
||||
while (!h.queue.isEmpty()) {
|
||||
h.queue.poll().run();
|
||||
h.builder.tick();
|
||||
}
|
||||
|
||||
assertEquals(List.of(10L, 20L, 30L), h.probe.captureOrder);
|
||||
assertEquals(List.of(0, 0, 0), h.probe.blocksOnCapture);
|
||||
}
|
||||
|
||||
@Test
|
||||
void slicesSurviveBurstsBeyondWorkerBound() {
|
||||
Harness h = new Harness(NO_WORKERS);
|
||||
|
||||
for (int i = 0; i < 5; i++) h.submit(false, i);
|
||||
while (!h.queue.isEmpty()) h.queue.poll().run();
|
||||
h.builder.tick();
|
||||
for (int i = 0; i < 5; i++) h.scheduler.acquireSlice();
|
||||
|
||||
assertEquals(5, h.slicesCreated.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void importantReleaseFailureSurfacesAndReturns() {
|
||||
Harness h = new Harness(NO_WORKERS);
|
||||
|
||||
DeferredSectionMesh m = h.submit(true, 0L);
|
||||
Mockito.doThrow(new IllegalStateException("release")).when(m.slice).releaseContext();
|
||||
h.scheduler.runImportant();
|
||||
assertEquals(0, h.results.size());
|
||||
|
||||
h.queue.poll().run();
|
||||
|
||||
assertEquals(1, h.results.size());
|
||||
assertTrue(expected.isInstance(h.results.peek()));
|
||||
assertEquals(0, h.scheduler.pendingCount());
|
||||
}
|
||||
|
||||
private static Stream<Arguments> normalResults() {
|
||||
return Stream.of(
|
||||
Arguments.of("success", (Supplier<ChunkJobResult<ChunkBuildOutput>>) DeferredMeshSchedulerTest::success, ChunkJobResult.Success.class),
|
||||
Arguments.of("failure is recorded and counters still decrement", (Supplier<ChunkJobResult<ChunkBuildOutput>>) DeferredMeshSchedulerTest::failure, ChunkJobResult.Failure.class));
|
||||
assertTrue(h.results.peek() instanceof ChunkJobResult.Failure);
|
||||
assertEquals(0, h.scheduler.finishingCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void importantItemAfterDestroyReleasesBuffers() {
|
||||
Harness h = new Harness();
|
||||
void releaseOrphansReleasesEachMeshOnce() {
|
||||
Harness h = new Harness(NO_WORKERS);
|
||||
|
||||
ChunkBuildBuffers buffers = h.scheduler.acquireBuffers();
|
||||
WorldSlice slice = h.scheduler.acquireSlice();
|
||||
DeferredSectionMesh m = mesh(buffers, slice, true, false, success());
|
||||
h.scheduler.submit(m);
|
||||
h.scheduler.markDestroyed();
|
||||
h.scheduler.runImportant();
|
||||
DeferredSectionMesh queued = h.submit(false, 2L);
|
||||
DeferredSectionMesh inFlight = h.submit(false, 1L);
|
||||
h.queue.poll().run();
|
||||
assertEquals(1, h.scheduler.finishingCount());
|
||||
|
||||
h.builder.shutdown();
|
||||
h.scheduler.releaseOrphans();
|
||||
h.scheduler.releaseOrphans();
|
||||
|
||||
assertEquals(0, h.scheduler.pendingCount());
|
||||
assertEquals(0, h.scheduler.finishingCount());
|
||||
assertEquals(0, h.results.size());
|
||||
assertEquals(0, h.scheduler.pendingCount());
|
||||
assertSame(buffers, h.scheduler.acquireBuffers());
|
||||
assertSame(slice, h.scheduler.acquireSlice());
|
||||
Mockito.verify(slice).releaseContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
void markDestroyedDiscardsQueuedItemAndReturnsBuffersToPool() {
|
||||
Harness h = new Harness();
|
||||
|
||||
ChunkBuildBuffers buffers = h.scheduler.acquireBuffers();
|
||||
DeferredSectionMesh m = mesh(buffers, h.scheduler.acquireSlice(), false, false, success());
|
||||
h.scheduler.submit(m);
|
||||
h.scheduler.markDestroyed();
|
||||
h.queue.poll().run();
|
||||
|
||||
assertEquals(0, h.results.size());
|
||||
assertEquals(0, h.scheduler.pendingCount());
|
||||
assertSame(buffers, h.scheduler.acquireBuffers());
|
||||
}
|
||||
|
||||
@Test
|
||||
void obsoleteItemIsDiscarded() {
|
||||
Harness h = new Harness();
|
||||
|
||||
ChunkBuildBuffers buffers = h.scheduler.acquireBuffers();
|
||||
DeferredSectionMesh m = mesh(buffers, h.scheduler.acquireSlice(), false, true, success());
|
||||
h.scheduler.submit(m);
|
||||
h.queue.poll().run();
|
||||
|
||||
assertEquals(0, h.results.size());
|
||||
assertEquals(0, h.scheduler.pendingCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void acquireBuffersAfterCompletedRunReturnsTheSameInstance() {
|
||||
Harness h = new Harness();
|
||||
|
||||
ChunkBuildBuffers buffers = h.scheduler.acquireBuffers();
|
||||
DeferredSectionMesh m = mesh(buffers, h.scheduler.acquireSlice(), false, false, success());
|
||||
h.scheduler.submit(m);
|
||||
h.queue.poll().run();
|
||||
|
||||
assertSame(buffers, h.scheduler.acquireBuffers());
|
||||
}
|
||||
|
||||
@Test
|
||||
void runReturnsSliceToPool() {
|
||||
Harness h = new Harness();
|
||||
|
||||
ChunkBuildBuffers buffers = h.scheduler.acquireBuffers();
|
||||
WorldSlice slice = h.scheduler.acquireSlice();
|
||||
DeferredSectionMesh m = mesh(buffers, slice, false, false, success());
|
||||
h.scheduler.submit(m);
|
||||
h.queue.poll().run();
|
||||
|
||||
assertSame(slice, h.scheduler.acquireSlice());
|
||||
Mockito.verify(slice).releaseContext();
|
||||
Mockito.verify(queued.slice, Mockito.times(1)).releaseContext();
|
||||
Mockito.verify(inFlight.slice, Mockito.times(1)).releaseContext();
|
||||
assertNull(inFlight.capture);
|
||||
}
|
||||
}
|
||||
+65
-1
@@ -8,10 +8,15 @@ import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.DeferredSectionMesh.DeferredCapture;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class DeferredSectionMeshTest {
|
||||
|
||||
@@ -26,7 +31,7 @@ class DeferredSectionMeshTest {
|
||||
@Override
|
||||
public void setCancelled() {}
|
||||
};
|
||||
return new DeferredSectionMesh(task, token, null, null, null, null, null, 0L, false, 0L);
|
||||
return new DeferredSectionMesh(task, token, null, null, null, null, null, 0L, false, 0L, 0L);
|
||||
}
|
||||
|
||||
static Stream<Arguments> sections() {
|
||||
@@ -45,4 +50,63 @@ class DeferredSectionMeshTest {
|
||||
setup.accept(section);
|
||||
assertEquals(expected, mesh(section, 5, cancelled).isObsolete());
|
||||
}
|
||||
|
||||
@Test
|
||||
void captureCopiesBlocks() {
|
||||
final int[] counts = {4, 12};
|
||||
final DeferredCapture capture = new DeferredCapture();
|
||||
final IntArrayList expectedRaw = new IntArrayList();
|
||||
final IntArrayList expectedStates = new IntArrayList();
|
||||
final IntArrayList expectedShader = new IntArrayList();
|
||||
final IntArrayList expectedStart = new IntArrayList();
|
||||
int total = 0;
|
||||
for (int b = 0; b < counts.length; b++) {
|
||||
final int n = counts[b];
|
||||
final int[] raw = new int[n * AngelicaChunkBuildContext.INTS_PER_VERTEX + 5];
|
||||
final int[] states = new int[n + 3];
|
||||
final int[] shader = new int[n + 3];
|
||||
for (int i = 0; i < raw.length; i++) raw[i] = b * 100000 + i;
|
||||
for (int i = 0; i < states.length; i++) {
|
||||
states[i] = b * 1000 + 500000 + i;
|
||||
shader[i] = b * 1000 + 900000 + i;
|
||||
}
|
||||
capture.add(b, raw, n, states, shader, 7000 + b);
|
||||
expectedStart.add(total);
|
||||
for (int i = 0; i < n * AngelicaChunkBuildContext.INTS_PER_VERTEX; i++) expectedRaw.add(raw[i]);
|
||||
for (int i = 0; i < n; i++) {
|
||||
expectedStates.add(states[i]);
|
||||
expectedShader.add(shader[i]);
|
||||
}
|
||||
total += n;
|
||||
}
|
||||
|
||||
assertEquals(counts.length, capture.blockCount());
|
||||
assertEquals(total, capture.vertexCount());
|
||||
assertEquals(expectedStart, capture.blockStart);
|
||||
assertEquals(expectedRaw, capture.raw);
|
||||
assertEquals(expectedStates, capture.vertexStates);
|
||||
assertEquals(expectedShader, capture.shaderBlockIds);
|
||||
for (int b = 0; b < counts.length; b++) {
|
||||
assertEquals(b, capture.blockIndex.getInt(b));
|
||||
assertEquals(counts[b], capture.blockVertexCount.getInt(b));
|
||||
assertEquals(7000 + b, capture.teBlockIds.getInt(b));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void clearEmptiesAndTrimsOversizedLists() {
|
||||
final DeferredCapture capture = new DeferredCapture();
|
||||
final int vertices = DeferredCapture.VERTEX_TRIM + 1000;
|
||||
capture.add(0, new int[vertices * AngelicaChunkBuildContext.INTS_PER_VERTEX], vertices, new int[vertices], new int[vertices], 1);
|
||||
assertTrue(capture.vertexStates.elements().length > DeferredCapture.VERTEX_TRIM);
|
||||
|
||||
capture.clear();
|
||||
|
||||
assertEquals(0, capture.raw.size());
|
||||
assertTrue(capture.raw.elements().length <= DeferredCapture.VERTEX_TRIM * AngelicaChunkBuildContext.INTS_PER_VERTEX);
|
||||
for (IntArrayList list : new IntArrayList[] {capture.vertexStates, capture.shaderBlockIds}) {
|
||||
assertEquals(0, list.size());
|
||||
assertTrue(list.elements().length <= DeferredCapture.VERTEX_TRIM);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.gtnewhorizons.angelica.rendering.celeritas;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
@@ -9,6 +10,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class ObjectPoolTest {
|
||||
@@ -20,7 +22,7 @@ class ObjectPoolTest {
|
||||
ObjectPool<Object> pool = new ObjectPool<>(() -> {
|
||||
calls.incrementAndGet();
|
||||
return new Object();
|
||||
});
|
||||
}, n);
|
||||
|
||||
Object[] acquired = new Object[n];
|
||||
for (int i = 0; i < n; i++) {
|
||||
@@ -36,4 +38,25 @@ class ObjectPoolTest {
|
||||
for (Object o : acquired) assertTrue(reacquired.contains(o));
|
||||
assertEquals(n, calls.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void releaseIsLifoAndDropsBeyondMaxFree() {
|
||||
AtomicInteger calls = new AtomicInteger();
|
||||
ObjectPool<Object> pool = new ObjectPool<>(() -> {
|
||||
calls.incrementAndGet();
|
||||
return new Object();
|
||||
}, 2);
|
||||
|
||||
Object a = pool.acquire();
|
||||
Object b = pool.acquire();
|
||||
Object c = pool.acquire();
|
||||
pool.release(a);
|
||||
pool.release(b);
|
||||
pool.release(c);
|
||||
|
||||
assertSame(b, pool.acquire());
|
||||
assertSame(a, pool.acquire());
|
||||
pool.acquire();
|
||||
assertEquals(4, calls.get());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user