Start to work on actual rendering side of things.

This commit is contained in:
TomTheFurry
2022-05-09 16:23:40 +08:00
parent ff181761c2
commit a872547d72
10 changed files with 522 additions and 11 deletions
@@ -24,7 +24,6 @@ public class DHLevel extends LodQuadTree {
}
}
@Override
public RenderDataSource getRenderDataSource() {
return dataHandler;
@@ -51,7 +51,8 @@ public abstract class LodQuadTree {
public void tick(DhBlockPos2D playerPos) {
for (int detailLevel = 0; detailLevel < maxPossibleDetailLevel; detailLevel++) {
ringLists[detailLevel].move(playerPos.x >> detailLevel, playerPos.z >> detailLevel);
ringLists[detailLevel].move(playerPos.x >> detailLevel, playerPos.z >> detailLevel,
LodSection::immediateDispose);
}
// First tick pass: update all sections' childCount from bottom level to top level. Step:
@@ -73,7 +74,6 @@ public abstract class LodQuadTree {
// - set childCount to -1 (Signal that this section will be freed if not rescued)
// - If targetLevel <= detail && section == null:
// - Parent's childCount++ (Create parent if needed)
for (byte detailLevel = 0; detailLevel < maxPossibleDetailLevel; detailLevel++) {
final MovableGridRingList<LodSection> ringList = ringLists[detailLevel];
final MovableGridRingList<LodSection> childRingList =
@@ -150,10 +150,8 @@ public abstract class LodQuadTree {
// if childCount == -1: // (section can be loaded or unloaded, due to fast movement)
// - set this section to null (TODO: Is this needed to be first or last or don't matter for concurrency?)
// - If loaded unload section
for (byte detailLevel = 0; detailLevel < maxPossibleDetailLevel; detailLevel++) {
final byte detail = detailLevel;
final MovableGridRingList<LodSection> ringList = ringLists[detail];
final MovableGridRingList<LodSection> ringList = ringLists[detailLevel];
final MovableGridRingList<LodSection> childRingList =
detailLevel == 0 ? null : ringLists[detailLevel - 1];
final MovableGridRingList<LodSection> parentRingList =
@@ -182,6 +180,7 @@ public abstract class LodQuadTree {
if (section.isLoaded()) {
section.unload();
}
section.dispose();
}
});
}
@@ -1,6 +1,8 @@
package com.seibel.lod.core.objects.a7;
import com.seibel.lod.core.objects.a7.pos.DhSectionPos;
import com.seibel.lod.core.objects.a7.render.RenderContainer;
import com.seibel.lod.core.util.LodUtil;
public class LodSection {
public static final int SUB_REGION_DATA_WIDTH = 16*16;
@@ -14,7 +16,7 @@ public class LodSection {
private RenderDataContainer levelContainer;
private RenderContainer renderContainer = null;
public RenderContainer renderContainer = null;
// Create sub region
public LodSection(DhSectionPos pos) {
@@ -34,10 +36,21 @@ public class LodSection {
}
public void unload() {
if (!isLoaded()) throw new IllegalStateException("LodSection is not loaded");
if (renderContainer != null) renderContainer.notifyUnload();
levelContainer = null;
}
public void dispose() {
LodUtil.assertTrue(!isLoaded());
if (renderContainer != null) renderContainer.notifyDispose();
}
public void immediateDispose() {
if (isLoaded()) unload();
if (renderContainer != null) renderContainer.notifyDispose();
}
public boolean isLoaded() {
return levelContainer != null;
}
}
@@ -1,4 +0,0 @@
package com.seibel.lod.core.objects.a7;
public class RenderContainer {
}
@@ -1,5 +1,6 @@
package com.seibel.lod.core.objects.a7.pos;
import com.seibel.lod.core.enums.LodDirection;
import com.seibel.lod.core.objects.a7.DHLevel;
import org.lwjgl.system.CallbackI;
@@ -50,4 +51,8 @@ public class DhSectionPos {
public DhSectionPos getParent(){
return new DhSectionPos((byte) (detail + 1), x / 2, z / 2);
}
public DhSectionPos getAdjacent(LodDirection dir) {
return new DhSectionPos(detail, x + dir.getNormal().x, z + dir.getNormal().z);
}
}
@@ -0,0 +1,39 @@
package com.seibel.lod.core.objects.a7.render;
import com.seibel.lod.core.objects.a7.pos.DhSectionPos;
import com.seibel.lod.core.objects.opengl.RenderBuffer;
import com.seibel.lod.core.objects.opengl.RenderRegion;
import com.seibel.lod.core.util.LodUtil;
public class ColumnRenderContainer extends RenderContainer {
public static final int columnWidth = DhSectionPos.DATA_WIDTH_PER_SECTION;
public static final int columnCount = LodUtil.pow2(DhSectionPos.DATA_WIDTH_PER_SECTION);
private long[] columnData;
public final int maxColumnHeight;
public final int minWorldHeight;
public RenderRegion renderRegion = null;
public ColumnRenderContainer(int maxColumnHeight, int minWorldHeight) {
this.maxColumnHeight = maxColumnHeight;
columnData = new long[columnCount * maxColumnHeight];
this.minWorldHeight = minWorldHeight;
renderRegion = new RenderRegion();
}
@Override
public void notifyUnload() {
}
@Override
public void notifyDispose() {
}
@Override
public RenderRegion getRenderRegion() {
return null;
}
}
@@ -0,0 +1,18 @@
package com.seibel.lod.core.objects.a7.render;
public class EmptyRenderContainer extends RenderContainer {
@Override
public void notifyUnload() {
}
@Override
public void notifyDispose() {
}
@Override
public boolean render() {
return true; //Always render successfully since there is nothing to render
}
}
@@ -0,0 +1,13 @@
package com.seibel.lod.core.objects.a7.render;
import com.seibel.lod.core.objects.opengl.RenderBuffer;
import com.seibel.lod.core.objects.opengl.RenderRegion;
public abstract class RenderContainer {
public abstract void notifyUnload();
public abstract void notifyDispose();
public abstract RenderRegion getRenderRegion();
}
@@ -0,0 +1,424 @@
/*
* This file is part of the Distant Horizons mod (formerly the LOD Mod),
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2022 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.lod.core.objects.a7.render;
import com.seibel.lod.core.api.internal.ClientApi;
import com.seibel.lod.core.builders.lodBuilding.LodBuilder;
import com.seibel.lod.core.builders.lodBuilding.bufferBuilding.CubicLodTemplate;
import com.seibel.lod.core.builders.lodBuilding.bufferBuilding.LodQuadBuilder;
import com.seibel.lod.core.enums.LodDirection;
import com.seibel.lod.core.enums.config.GpuUploadMethod;
import com.seibel.lod.core.enums.rendering.DebugMode;
import com.seibel.lod.core.enums.rendering.GLProxyContext;
import com.seibel.lod.core.handlers.dependencyInjection.SingletonHandler;
import com.seibel.lod.core.objects.*;
import com.seibel.lod.core.objects.a7.LodQuadTree;
import com.seibel.lod.core.objects.a7.LodSection;
import com.seibel.lod.core.objects.lod.LodDimension;
import com.seibel.lod.core.objects.lod.LodRegion;
import com.seibel.lod.core.objects.math.Vec3d;
import com.seibel.lod.core.objects.math.Vec3f;
import com.seibel.lod.core.objects.opengl.RenderBuffer;
import com.seibel.lod.core.objects.opengl.SimpleRenderBuffer;
import com.seibel.lod.core.render.GLProxy;
import com.seibel.lod.core.render.LodRenderProgram;
import com.seibel.lod.core.render.RenderUtil;
import com.seibel.lod.core.util.DataPointUtil;
import com.seibel.lod.core.util.LevelPosUtil;
import com.seibel.lod.core.util.LodUtil;
import com.seibel.lod.core.util.StatsMap;
import com.seibel.lod.core.util.gridList.PosArrayGridList;
import com.seibel.lod.core.wrapperInterfaces.config.ILodConfigWrapperSingleton;
import java.util.ConcurrentModificationException;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import static com.seibel.lod.core.render.LodRenderer.EVENT_LOGGER;
public class RenderSection implements AutoCloseable
{
private static final ILodConfigWrapperSingleton CONFIG = SingletonHandler.get(ILodConfigWrapperSingleton.class);
/** stores if the region at the given x and z index needs to be regenerated */
// Use int because I need Tri state:
private final AtomicInteger needRegen = new AtomicInteger(2);
private enum BackState {
Unused,
Building,
Complete,
}
private enum FrontState {
Unused,
Rendering,
Invalidated,
}
final LodSection lodSection;
final LodQuadTree quadTree;
RenderBuffer renderBufferBack = null;
AtomicReference<BackState> backState =
new AtomicReference<BackState>(BackState.Unused);
AtomicReference<FrontState> frontState =
new AtomicReference<FrontState>(FrontState.Unused);
RenderBuffer renderBufferFront = null;
public RenderSection(LodSection lodSection, LodQuadTree quadTree) {
this.lodSection = lodSection;
this.quadTree = quadTree;
}
public void setNeedRegen() {
needRegen.set(2);
}
public Optional<CompletableFuture<Void>> updateStatus(Executor bufferUploader, Executor bufferBuilder, boolean alwaysRegen, int playerPosX, int playerPosZ, boolean doCaveCulling) {
if (alwaysRegen) setNeedRegen();
BackState state = backState.get();
if (state != BackState.Unused) {
EVENT_LOGGER.trace("{}: UpdateStatus rejected. Cause: BackState is {}", lodSection, state);
return Optional.empty();
}
if (needRegen.get() == 0) {
EVENT_LOGGER.trace("{}: UpdateStatus rejected. Cause: Region doesn't need regen", lodSection);
return Optional.empty();
}
if (!backState.compareAndSet(BackState.Unused, BackState.Building)) {
EVENT_LOGGER.trace("{}: UpdateStatus rejected. Cause: CAS on BackState failed: ", backState.get());
return Optional.empty();
}
needRegen.decrementAndGet();
return Optional.of(startBuid(bufferUploader, bufferBuilder, lodSection, playerPosX, playerPosZ, doCaveCulling));
}
public boolean render(LodDimension renderDim,
Vec3d cameraPos, DHBlockPos cameraBlockPos, Vec3f cameraDir,
boolean enableDirectionalCulling, LodRenderProgram program) {
if (!frontState.compareAndSet(FrontState.Unused, FrontState.Rendering)) return false;
try {
if (renderDim != lodDim) return false;
if (enableDirectionalCulling &&
!RenderUtil.isRegionInViewFrustum(cameraBlockPos,
cameraDir, regionPos.x, regionPos.z)) return false;
BackState state = backState.get();
if (state == BackState.Complete) {
if (renderBufferBack != null) {
EVENT_LOGGER.debug("RenderRegion swap @ {}", regionPos);
boolean shouldKeep = renderBufferFront != null && renderBufferFront.onSwapToBack();
RenderBuffer temp = shouldKeep ? renderBufferFront : null;
renderBufferFront = renderBufferBack;
renderBufferBack = temp;
if (renderBufferFront != null) renderBufferFront.onSwapToFront();
}
if (!backState.compareAndSet(BackState.Complete, BackState.Unused)) {
EVENT_LOGGER.error("RenderRegion.render() got illegal state on swapping buffer!");
}
}
if (renderBufferFront == null) return false;
program.setModelPos(new Vec3f(
(float) ((regionPos.x * LodUtil.REGION_WIDTH) - cameraPos.x),
(float) (LodBuilder.MIN_WORLD_HEIGHT - cameraPos.y),
(float) ((regionPos.z * LodUtil.REGION_WIDTH) - cameraPos.z)));
return renderBufferFront.render(program);
} finally {
frontState.compareAndSet(FrontState.Rendering, FrontState.Unused);
}
}
private void recreateBuffer(LodQuadBuilder builder) {
if (renderBufferBack != null) throw new RuntimeException("Assert Error");
boolean useSimpleBuffer = (builder.getCurrentNeededVertexBufferCount() <= 6) || true;
renderBufferBack = useSimpleBuffer ?
new SimpleRenderBuffer()
: null; //new ComplexRenderRegion(regPos);
}
private CompletableFuture<Void> startBuid(Executor bufferUploader, Executor bufferBuilder, LodSection section, int playerPosX, int playerPosZ, boolean doCaveCulling) {
EVENT_LOGGER.trace("RenderSection startBuild @ {}", section);
RenderContainer[] adjSections = new RenderContainer[4];
try {
if (renderBufferBack != null) renderBufferBack.onReuse();
for (LodDirection dir : LodDirection.ADJ_DIRECTIONS) {
adjSections[dir.ordinal() - 2] = quadTree.getSection(section.pos.getAdjacent(dir)).renderContainer;
}
} catch (Throwable t) {
setNeedRegen();
if (!backState.compareAndSet(BackState.Building, BackState.Unused)) {
EVENT_LOGGER.error("\"Lod Builder Starter\""
+ " encountered error on catching exceptions and fallback on starting build task: ",
new ConcurrentModificationException("RenderRegion Illegal State"));
}
throw t;
}
return CompletableFuture.supplyAsync(() -> {
try {
EVENT_LOGGER.trace("RenderRegion start QuadBuild @ {}", regionPos);
int skyLightCullingBelow = CONFIG.client().graphics().advancedGraphics().getCaveCullingHeight();
// FIXME: Clamp also to the max world height.
skyLightCullingBelow = Math.max(skyLightCullingBelow, LodBuilder.MIN_WORLD_HEIGHT);
LodQuadBuilder builder = new LodQuadBuilder(doCaveCulling, skyLightCullingBelow);
Runnable buildRun = ()->{
makeLodRenderData(builder, region, adjRegions, playerPosX, playerPosZ);
};
if (renderBufferBack != null)
renderBufferBack.build(buildRun);
else
buildRun.run();
EVENT_LOGGER.trace("RenderRegion end QuadBuild @ {}", regionPos);
return builder;
} catch (Throwable e3) {
EVENT_LOGGER.error("\"LodNodeBufferBuilder\" was unable to build quads: ", e3);
throw e3;
}
}, bufferBuilder)
.thenAcceptAsync((builder) -> {
try {
EVENT_LOGGER.trace("RenderRegion start Upload @ {}", regionPos);
GLProxy glProxy = GLProxy.getInstance();
GpuUploadMethod method = GLProxy.getInstance().getGpuUploadMethod();
GLProxyContext oldContext = glProxy.getGlContext();
glProxy.setGlContext(GLProxyContext.LOD_BUILDER);
try {
if (renderBufferBack == null) recreateBuffer(builder);
if (!renderBufferBack.tryUploadBuffers(builder, method)) {
renderBufferBack = null;
recreateBuffer(builder);
if (!renderBufferBack.tryUploadBuffers(builder, method)) {
throw new RuntimeException("Newly created renderBuffer "
+ "is still returning false on tryUploadBuffers!");
}
}
} finally {
glProxy.setGlContext(oldContext);
}
EVENT_LOGGER.trace("RenderRegion end Upload @ {}", regionPos);
} catch (Throwable e3) {
EVENT_LOGGER.error("\"LodNodeBufferBuilder\" was unable to upload buffer: ", e3);
throw e3;
}
}, bufferUploader).handle((v, e) -> {
if (e != null) {
setNeedRegen();
if (!backState.compareAndSet(BackState.Building, BackState.Unused)) {
EVENT_LOGGER.error("\"LodNodeBufferBuilder\""
+ " encountered error on exit: ",
new ConcurrentModificationException("RenderRegion Illegal State"));
}
} else {
if (!backState.compareAndSet(BackState.Building, BackState.Complete)) {
EVENT_LOGGER.error("\"LodNodeBufferBuilder\""
+ " encountered error on exit: ",
new ConcurrentModificationException("RenderRegion Illegal State"));
}
}
return (Void) null;
});
}
private static final int ADJACENT8[][] = {
{-1,-1},
{-1, 0},
{-1, 1},
{ 0,-1},
//{ 0, 0},
{ 0, 1},
{ 1,-1},
{ 1, 0},
{ 1, 1}
};
private static void makeLodRenderData(LodQuadBuilder quadBuilder, LodRegion region, LodRegion[] adjRegions, int playerX,
int playerZ) {
byte minDetail = region.getMinDetailLevel();
// Variable initialization
DebugMode debugMode = CONFIG.client().advanced().debugging().getDebugMode();
// We ask the lod dimension which block we have to render given the player
// position
PosToRenderContainer posToRender = new PosToRenderContainer(minDetail, region.regionPosX, region.regionPosZ);
region.getPosToRender(posToRender, playerX, playerZ);
posToRender.sort();
PosArrayGridList<BoolType> chunkGrid = ClientApi.renderer.vanillaChunks;
for (int index = 0; index < posToRender.getNumberOfPos(); index++) {
byte detailLevel = posToRender.getNthDetailLevel(index);
int posX = posToRender.getNthPosX(index);
int posZ = posToRender.getNthPosZ(index);
// TODO: In the future, We don't need to ignore rendered chunks! Just build it
// and leave it for the renderer to decide!
// We don't want to render this fake block if
// The block is inside the render distance with, is not bigger than a chunk and
// is positioned in a chunk set as vanilla rendered
// The block is in the player chunk or in a chunk adjacent to the player
if (detailLevel <= LodUtil.CHUNK_DETAIL_LEVEL) {
int chunkX = LevelPosUtil.getChunkPos(detailLevel, posX);
int chunkZ = LevelPosUtil.getChunkPos(detailLevel, posZ);
// skip any chunks that Minecraft is going to render
if (chunkGrid != null && chunkGrid.get(chunkX, chunkZ) != null) continue;
}
LodDataView posData = region.getDataView(detailLevel, posX, posZ);
if (posData == null || posData.size() == 0 || !DataPointUtil.doesItExist(posData.get(0))
|| DataPointUtil.isVoid(posData.get(0)))
continue;
LodDataView[][] adjData = new LodDataView[4][];
boolean[] adjUseBlack = new boolean[4];
// We extract the adj data in the four cardinal direction
// we first reset the adjShadeDisabled. This is used to disable the shade on the
// border when we have transparent block like water or glass
// to avoid having a "darker border" underground
// Arrays.fill(adjShadeDisabled, false);
// We check every adj block in each direction
// If the adj block is rendered in the same region and with same detail
// and is positioned in a place that is not going to be rendered by vanilla game
// then we can set this position as adj
// We avoid cases where the adjPosition is in player chunk while the position is
// not
// to always have a wall underwater
for (LodDirection lodDirection : LodDirection.ADJ_DIRECTIONS) {
try {
int xAdj = posX + lodDirection.getNormal().x;
int zAdj = posZ + lodDirection.getNormal().z;
int chunkXAdj = LevelPosUtil.getChunkPos(detailLevel, xAdj);
int chunkZAdj = LevelPosUtil.getChunkPos(detailLevel, zAdj);
if (chunkGrid != null && chunkGrid.get(chunkXAdj, chunkZAdj)!=null) {
adjUseBlack[lodDirection.ordinal()-2] = true;
}
boolean isCrossRegionBoundary = LevelPosUtil.getRegion(detailLevel, xAdj) != region.regionPosX ||
LevelPosUtil.getRegion(detailLevel, zAdj) != region.regionPosZ;
LodRegion adjRegion;
byte adjDetail;
int childXAdj = xAdj*2 + (lodDirection.getNormal().x<0 ? 1 : 0);
int childZAdj = zAdj*2 + (lodDirection.getNormal().z<0 ? 1 : 0);
//we check if the detail of the adjPos is equal to the correct one (region border fix)
//or if the detail is wrong by 1 value (region+circle border fix)
if (isCrossRegionBoundary) {
//we compute at which detail that position should be rendered
adjRegion = adjRegions[lodDirection.ordinal()-2];
if(adjRegion == null) continue;
adjDetail = adjRegion.getRenderDetailLevelAt(playerX, playerZ, detailLevel, xAdj, zAdj);
} else {
adjRegion = region;
if (posToRender.contains(detailLevel, xAdj, zAdj)) adjDetail = detailLevel;
else if (detailLevel>0 &&
posToRender.contains((byte) (detailLevel-1), childXAdj, childZAdj))
adjDetail = (byte) (detailLevel-1);
else if (detailLevel<LodUtil.REGION_DETAIL_LEVEL &&
posToRender.contains((byte) (detailLevel+1), xAdj/2, zAdj/2))
adjDetail = (byte) (detailLevel+1);
else continue;
}
if (adjDetail < detailLevel-1 || adjDetail > detailLevel+1) {
continue;
}
if (adjDetail == detailLevel || adjDetail > detailLevel) {
adjData[lodDirection.ordinal() - 2] = new LodDataView[1];
adjData[lodDirection.ordinal() - 2][0] = adjRegion.getDataView(adjDetail,
LevelPosUtil.convert(detailLevel, xAdj, adjDetail),
LevelPosUtil.convert(detailLevel, zAdj, adjDetail));
} else {
adjData[lodDirection.ordinal() - 2] = new LodDataView[2];
adjData[lodDirection.ordinal() - 2][0] = adjRegion.getDataView(adjDetail,
childXAdj, childZAdj);
adjData[lodDirection.ordinal() - 2][1] = adjRegion.getDataView(adjDetail,
childXAdj + (lodDirection.getAxis()==LodDirection.Axis.X ? 0 : 1),
childZAdj + (lodDirection.getAxis()==LodDirection.Axis.Z ? 0 : 1));
}
} catch (RuntimeException e) {
EVENT_LOGGER.warn("Failed to get adj data for [{}:{},{}] at [{}]", detailLevel, posX, posZ, lodDirection);
EVENT_LOGGER.warn("Detail exception: ", e);
}
}
// We render every vertical lod present in this position
// We only stop when we find a block that is void or non-existing block
for (int i = 0; i < posData.size(); i++) {
long data = posData.get(i);
// If the data is not renderable (Void or non-existing) we stop since there is
// no data left in this position
if (DataPointUtil.isVoid(data) || !DataPointUtil.doesItExist(data))
break;
long adjDataTop = i - 1 >= 0 ? posData.get(i - 1) : DataPointUtil.EMPTY_DATA;
long adjDataBot = i + 1 < posData.size() ? posData.get(i + 1) : DataPointUtil.EMPTY_DATA;
// We send the call to create the vertices
CubicLodTemplate.addLodToBuffer(data, adjDataTop, adjDataBot, adjData, adjUseBlack, detailLevel,
LevelPosUtil.getRegionModule(detailLevel, posX),
LevelPosUtil.getRegionModule(detailLevel, posZ), quadBuilder, debugMode);
}
} // for pos to in list to render
// the thread executed successfully
// Merge all quads
quadBuilder.mergeQuads();
}
@Override
public void close()
{
if (renderBufferBack != null) renderBufferBack.close();
while (frontState.get() != FrontState.Invalidated && !frontState.compareAndSet(FrontState.Unused, FrontState.Invalidated)) {
Thread.yield(); //FIXME: If on java 9, use Thread.onSpinWait();
}
if (renderBufferFront != null) renderBufferFront.close();
}
public void debugDumpStats(StatsMap statsMap)
{
statsMap.incStat("RenderRegions");
RenderBuffer front = renderBufferFront;
if (front!=null) {
statsMap.incStat("FrontBuffers");
front.debugDumpStats(statsMap);
}
RenderBuffer back = renderBufferBack;
if (back!=null) {
statsMap.incStat("BackBuffers");
back.debugDumpStats(statsMap);
}
}
}
@@ -0,0 +1,5 @@
package com.seibel.lod.core.objects.a7.render.column;
public class ColumnRenderBuffer {
}