refactor RenderBufferHandler

This commit is contained in:
James Seibel
2023-02-17 14:37:14 -06:00
parent 0e9f7eb653
commit c95b0849e3
@@ -13,241 +13,332 @@ import com.seibel.lod.core.util.objects.SortedArraySet;
import java.util.Comparator;
import java.util.concurrent.atomic.AtomicReference;
public class RenderBufferHandler {
public final LodQuadTree target;
public class RenderBufferHandler
{
public final LodQuadTree quadTree;
private final MovableGridRingList<RenderBufferNode> renderBufferNodes;
// TODO: Make sorting go into the update loop instead of the render loop as it doesn't need to be done every frame
private SortedArraySet<LoadedRenderBuffer> loadedNearToFarBuffers = null;
public RenderBufferHandler(LodQuadTree quadTree)
{
this.quadTree = quadTree;
MovableGridRingList<LodRenderSection> referenceList = quadTree.getRingList((byte) (quadTree.getNumbersOfSectionDetailLevels() - 1));
Pos2D center = referenceList.getCenter();
this.renderBufferNodes = new MovableGridRingList<>(referenceList.getHalfSize(), center);
}
/**
* The following buildRenderList sorting method is based on the following reddit post:
* https://www.reddit.com/r/VoxelGameDev/comments/a0l8zc/correct_depthordering_for_translucent_discrete/
*
* TODO: This might get locked by update() causing move() call. Is there a way to avoid this?
* Maybe dupe the base list and use atomic swap on render? Or is this not worth it?
*/
public void buildRenderList(Vec3f lookForwardVector)
{
ELodDirection[] axisDirections = new ELodDirection[3];
// Do the axis that are longest first (i.e. the largest absolute value of the lookForwardVector),
// with the sign being the opposite of the respective lookForwardVector component's sign
float absX = Math.abs(lookForwardVector.x);
float absY = Math.abs(lookForwardVector.y);
float absZ = Math.abs(lookForwardVector.z);
ELodDirection xDir = lookForwardVector.x < 0 ? ELodDirection.EAST : ELodDirection.WEST;
ELodDirection yDir = lookForwardVector.y < 0 ? ELodDirection.UP : ELodDirection.DOWN;
ELodDirection zDir = lookForwardVector.z < 0 ? ELodDirection.SOUTH : ELodDirection.NORTH;
if (absX >= absY && absX >= absZ)
{
axisDirections[0] = xDir;
if (absY >= absZ)
{
axisDirections[1] = yDir;
axisDirections[2] = zDir;
}
else
{
axisDirections[1] = zDir;
axisDirections[2] = yDir;
}
}
else if (absY >= absX && absY >= absZ)
{
axisDirections[0] = yDir;
if (absX >= absZ)
{
axisDirections[1] = xDir;
axisDirections[2] = zDir;
}
else
{
axisDirections[1] = zDir;
axisDirections[2] = xDir;
}
}
else
{
axisDirections[0] = zDir;
if (absX >= absY)
{
axisDirections[1] = xDir;
axisDirections[2] = yDir;
}
else
{
axisDirections[1] = yDir;
axisDirections[2] = xDir;
}
}
// Now that we have the axis directions, we can sort the render list
Comparator<LoadedRenderBuffer> sortFarToNear = (loadedBufferA, loadedBufferB) ->
{
Pos2D aPos = loadedBufferA.pos.getCenter().getCenterBlockPos().toPos2D();
Pos2D bPos = loadedBufferB.pos.getCenter().getCenterBlockPos().toPos2D();
for (ELodDirection axisDirection : axisDirections)
{
if (axisDirection.getAxis().isVertical())
{
continue; // We only sort in the horizontal direction
}
int abPosDifference;
if (axisDirection.getAxis().equals(ELodDirection.Axis.X))
{
abPosDifference = aPos.x - bPos.x;
}
else
{
abPosDifference = aPos.y - bPos.y;
}
if (abPosDifference == 0)
{
continue;
}
if (axisDirection.getAxisDirection().equals(ELodDirection.AxisDirection.NEGATIVE))
{
abPosDifference = -abPosDifference; // Reverse the sign
}
return abPosDifference;
}
return loadedBufferA.pos.sectionDetailLevel - loadedBufferB.pos.sectionDetailLevel; // If all else fails, sort by detail
};
// Build the sorted list
this.loadedNearToFarBuffers = new SortedArraySet<>((a, b) -> -sortFarToNear.compare(a, b));
// Add all the loaded buffers to the sorted list
this.renderBufferNodes.forEach((renderBufferNode) ->
{
if (renderBufferNode != null)
{
renderBufferNode.collect(this.loadedNearToFarBuffers);
}
});
}
public void renderOpaque(LodRenderer renderContext)
{
//TODO: Directional culling
this.loadedNearToFarBuffers.forEach(loadedBuffer -> loadedBuffer.buffer.renderOpaque(renderContext));
}
public void renderTransparent(LodRenderer renderContext)
{
if (LodRenderer.transparencyEnabled)
{
this.loadedNearToFarBuffers.forEach(loadedBuffer -> loadedBuffer.buffer.renderTransparent(renderContext));
}
}
public void update()
{
byte topDetailLevel = (byte) (this.quadTree.getNumbersOfSectionDetailLevels() - 1);
MovableGridRingList<LodRenderSection> renderSectionGridList = this.quadTree.getRingList(topDetailLevel);
Pos2D center = renderSectionGridList.getCenter();
//boolean moved = renderBufferNodes.getCenterBlockPos().x != center.x || renderBufferNodes.getCenterBlockPos().y != center.y;
this.renderBufferNodes.moveTo(center.x, center.y, RenderBufferNode::close); // Note: may lock the list
this.renderBufferNodes.forEachPosOrdered((renderBufferNode, pos) ->
{
DhSectionPos sectPos = new DhSectionPos(topDetailLevel, pos.x, pos.y);
LodRenderSection section = this.quadTree.getSection(sectPos);
if (section == null)
{
// If section is null, but node exists, remove node
if (renderBufferNode != null)
{
this.renderBufferNodes.remove(pos).close();
}
// If section is null, continue
return;
}
// If section is not null, but node does not exist, create node
if (renderBufferNode == null)
{
renderBufferNode = this.renderBufferNodes.setChained(pos, new RenderBufferNode(sectPos));
}
// Update node
renderBufferNode.update();
});
}
private static class LoadedRenderBuffer {
public final AbstractRenderBuffer buffer;
public final DhSectionPos pos;
LoadedRenderBuffer(AbstractRenderBuffer buffer, DhSectionPos pos) {
this.buffer = buffer;
this.pos = pos;
}
}
// TODO: Make sorting go into the update loop instead of the render loop as it doesn't need to be done every frame
private SortedArraySet<LoadedRenderBuffer> loadedNearToFarBuffers = null;
// The followiing buildRenderList sorting method is based on the following reddit post:
// https://www.reddit.com/r/VoxelGameDev/comments/a0l8zc/correct_depthordering_for_translucent_discrete/
public void buildRenderList(Vec3f lookForwardVector) {
ELodDirection[] axisDirections = new ELodDirection[3];
// Do the axis that are longest first (i.e. the largest absolute value of the lookForwardVector)
// , with the sign being the opposite of the respective lookForwardVector component's sign
float absX = Math.abs(lookForwardVector.x);
float absY = Math.abs(lookForwardVector.y);
float absZ = Math.abs(lookForwardVector.z);
ELodDirection xDir = lookForwardVector.x < 0 ? ELodDirection.EAST : ELodDirection.WEST;
ELodDirection yDir = lookForwardVector.y < 0 ? ELodDirection.UP : ELodDirection.DOWN;
ELodDirection zDir = lookForwardVector.z < 0 ? ELodDirection.SOUTH : ELodDirection.NORTH;
if (absX >= absY && absX >= absZ) {
axisDirections[0] = xDir;
if (absY >= absZ) {
axisDirections[1] = yDir;
axisDirections[2] = zDir;
} else {
axisDirections[1] = zDir;
axisDirections[2] = yDir;
}
} else if (absY >= absX && absY >= absZ) {
axisDirections[0] = yDir;
if (absX >= absZ) {
axisDirections[1] = xDir;
axisDirections[2] = zDir;
} else {
axisDirections[1] = zDir;
axisDirections[2] = xDir;
}
} else {
axisDirections[0] = zDir;
if (absX >= absY) {
axisDirections[1] = xDir;
axisDirections[2] = yDir;
} else {
axisDirections[1] = yDir;
axisDirections[2] = xDir;
}
}
// Now that we have the axis directions, we can sort the render list
Comparator<LoadedRenderBuffer> sortFarToNear = (a, b) -> {
Pos2D aPos = a.pos.getCenter().getCenterBlockPos().toPos2D();
Pos2D bPos = b.pos.getCenter().getCenterBlockPos().toPos2D();
for (ELodDirection axisDirection : axisDirections) {
if (axisDirection.getAxis().isVertical()) continue; // We works on the horizontal plane only for section sorting
int abDiff;
if (axisDirection.getAxis().equals(ELodDirection.Axis.X)) {
abDiff = aPos.x - bPos.x;
} else {
abDiff = aPos.y - bPos.y;
}
if (abDiff == 0) continue;
if (axisDirection.getAxisDirection().equals(ELodDirection.AxisDirection.NEGATIVE)) {
abDiff = -abDiff; // Reverse the sign
}
return abDiff;
}
return a.pos.sectionDetailLevel - b.pos.sectionDetailLevel; // If all else fails, sort by detail
};
Comparator<LoadedRenderBuffer> sortNearToFar = (a, b) -> -sortFarToNear.compare(a, b);
// Build the sorted list
loadedNearToFarBuffers = new SortedArraySet<>(sortNearToFar);
// Add all the loaded buffers to the sorted list
renderBufferNodes.forEach((r) -> {if (r!=null) r.collect(loadedNearToFarBuffers);});
}
class RenderBufferNode implements AutoCloseable {
public final DhSectionPos pos;
public volatile RenderBufferNode[] children = null;
//FIXME: The multiple Atomics will cause race conditions between them!
public final AtomicReference<AbstractRenderBuffer> renderBufferSlot = new AtomicReference<>();
public RenderBufferNode(DhSectionPos pos) {
this.pos = pos;
}
public void collect(SortedArraySet<LoadedRenderBuffer> sortedSet) {
AbstractRenderBuffer buff;
buff = renderBufferSlot.get();
if (buff != null) {
sortedSet.add(new LoadedRenderBuffer(buff, pos));
} else {
RenderBufferNode[] childs = children;
if (childs != null) {
for (RenderBufferNode child : childs) {
child.collect(sortedSet);
}
}
}
}
//TODO: In the future make this logic a bit more complex so that when children are just created,
// the buffer is only unloaded if all children's buffers are ready. This will make the
// transition between buffers no longer causing any flicker.
public void update() {
LodRenderSection section = target.getSection(pos);
// If this fails, there may be concurrent modification of the quad tree
// (as this update() should be called from the same thread that calls update() on the quad tree)
LodUtil.assertTrue(section != null);
IRenderSource container = section.getRenderSource();
// Update self's render buffer state
boolean shouldRender = section.canRender();
if (!shouldRender) {
//TODO: Does this really need to force the old buffer to not be rendered?
AbstractRenderBuffer buff = renderBufferSlot.getAndSet(null);
if (buff != null) {
buff.close();
}
} else {
LodUtil.assertTrue(container != null); // section.isLoaded() should have ensured this
container.trySwapRenderBuffer(target, renderBufferSlot);
}
// Update children's render buffer state
// TODO: Improve this! (Checking section.isLoaded() as if its not loaded, it can only be because
// it has children. (But this logic is... really hard to read!)
// FIXME: Above comment is COMPLETELY WRONG! I am an idiot!
boolean shouldHaveChildren = section.FIXME_BYPASS_DONT_USE_getChildCount() > 0;
if (shouldHaveChildren) {
if (children == null) {
RenderBufferNode[] childs = new RenderBufferNode[4];
for (int i = 0; i < 4; i++) {
childs[i] = new RenderBufferNode(pos.getChildByIndex(i));
}
children = childs;
}
for (RenderBufferNode child : children) {
child.update();
}
} else {
if (children != null) {
//FIXME: Concurrency issue here: If render thread is concurrently using the child's buffer,
// and this thread got priority to close the buffer, it causes a bug where the render thread
// will be using a closed buffer!!!!
RenderBufferNode[] childs = children;
children = null;
for (RenderBufferNode child : childs) {
child.close();
}
}
}
}
@Override
public void close() {
if (children != null) {
for (RenderBufferNode child : children) {
child.close();
}
}
AbstractRenderBuffer buff;
buff = renderBufferSlot.getAndSet(null);
if (buff != null) {
buff.close();
}
}
}
public RenderBufferHandler(LodQuadTree target) {
this.target = target;
MovableGridRingList<LodRenderSection> referenceList = target.getRingList((byte) (target.getNumbersOfSectionDetailLevels() - 1));
Pos2D center = referenceList.getCenter();
renderBufferNodes = new MovableGridRingList<>(referenceList.getHalfSize(), center);
}
//TODO: This might get locked by update() causing move() call. Is there a way to avoid this?
// Maybe dupe the base list and use atomic swap on render? Or is this not worth it?
public void prepare(LodRenderer renderContext) {
buildRenderList(renderContext.getLookVector());
}
public void renderOpaque(LodRenderer renderContext) {
//TODO: Directional culling
loadedNearToFarBuffers.forEach(b -> b.buffer.renderOpaque(renderContext));
}
public void renderTransparent(LodRenderer renderContext) {
if(LodRenderer.transparencyEnabled)
loadedNearToFarBuffers.forEach(b -> b.buffer.renderTransparent(renderContext));
}
public void update() {
byte topDetail = (byte) (target.getNumbersOfSectionDetailLevels() - 1);
MovableGridRingList<LodRenderSection> referenceList = target.getRingList(topDetail);
Pos2D center = referenceList.getCenter();
//boolean moved = renderBufferNodes.getCenterBlockPos().x != center.x || renderBufferNodes.getCenterBlockPos().y != center.y;
renderBufferNodes.moveTo(center.x, center.y, RenderBufferNode::close); // Note: may lock the list
renderBufferNodes.forEachPosOrdered((node, pos) -> {
DhSectionPos sectPos = new DhSectionPos(topDetail, pos.x, pos.y);
LodRenderSection section = target.getSection(sectPos);
if (section == null) {
// If section is null, but node exists, remove node
if (node != null) {
renderBufferNodes.remove(pos).close();
}
// If section is null, continue
return;
}
// If section is not null, but node does not exist, create node
if (node == null) {
node = renderBufferNodes.setChained(pos, new RenderBufferNode(sectPos));
}
// Node should be not null here
// Update node
node.update();
});
}
public void close() {
renderBufferNodes.clear(RenderBufferNode::close);
}
public void close() { this.renderBufferNodes.clear(RenderBufferNode::close); }
//================//
// helper classes //
//================//
private static class LoadedRenderBuffer
{
public final AbstractRenderBuffer buffer;
public final DhSectionPos pos;
LoadedRenderBuffer(AbstractRenderBuffer buffer, DhSectionPos pos)
{
this.buffer = buffer;
this.pos = pos;
}
}
private class RenderBufferNode implements AutoCloseable
{
public final DhSectionPos pos;
public volatile RenderBufferNode[] children = null;
//FIXME: The multiple Atomics will cause race conditions between them!
public final AtomicReference<AbstractRenderBuffer> renderBufferRef = new AtomicReference<>();
public RenderBufferNode(DhSectionPos pos) { this.pos = pos; }
public void collect(SortedArraySet<LoadedRenderBuffer> sortedSet)
{
AbstractRenderBuffer renderBuffer = this.renderBufferRef.get();
if (renderBuffer != null)
{
sortedSet.add(new LoadedRenderBuffer(renderBuffer, this.pos));
}
else
{
RenderBufferNode[] children = this.children;
if (children != null)
{
for (RenderBufferNode child : children)
{
child.collect(sortedSet);
}
}
}
}
//TODO: In the future make this logic a bit more complete so that when children are just created,
// the buffer is only unloaded if all children's buffers are ready. This will make the
// transition between buffers no longer causing any flicker.
public void update()
{
LodRenderSection section = quadTree.getSection(this.pos);
// If this fails, there may be concurrent modification of the quad tree
// (as this update() should be called from the same thread that calls update() on the quad tree)
LodUtil.assertTrue(section != null);
IRenderSource currentRenderSource = section.getRenderSource();
// Update self's render buffer state
boolean shouldRender = section.shouldRender();
if (!shouldRender)
{
//TODO: Does this really need to force the old buffer to not be rendered?
AbstractRenderBuffer renderBuffer = this.renderBufferRef.getAndSet(null);
if (renderBuffer != null)
{
renderBuffer.close();
}
}
else
{
LodUtil.assertTrue(currentRenderSource != null); // section.isLoaded() should have ensured this
currentRenderSource.trySwapRenderBufferAsync(quadTree, this.renderBufferRef);
}
// Update children's render buffer state
// TODO: Improve this! (Checking section.isLoaded() as if its not loaded, it can only be because
// it has children. (But this logic is... really hard to read!)
// FIXME: Above comment is COMPLETELY WRONG! I am an idiot!
boolean sectionHasChildren = section.FIXME_BYPASS_DONT_USE_getChildCount() > 0;
if (sectionHasChildren)
{
if (this.children == null)
{
RenderBufferNode[] children = new RenderBufferNode[4];
for (int i = 0; i < 4; i++)
{
children[i] = new RenderBufferNode(this.pos.getChildByIndex(i));
}
this.children = children;
}
for (RenderBufferNode child : this.children)
{
child.update();
}
}
else
{
// if (this.children != null)
// {
// //FIXME: Concurrency issue here: If render thread is concurrently using the child's buffer,
// // and this thread got priority to close the buffer, it causes a bug where the render thread
// // will be using a closed buffer!!!!
// RenderBufferNode[] children = this.children;
// this.children = null;
// for (RenderBufferNode child : children)
// {
// child.close();
// }
// }
}
}
@Override
public void close()
{
if (children != null)
{
for (RenderBufferNode child : children)
{
child.close();
}
}
AbstractRenderBuffer buff;
buff = this.renderBufferRef.getAndSet(null);
if (buff != null)
{
buff.close();
}
}
}
}