feat(renderer): Refactor rendering object pool management and update related tests

This commit is contained in:
extio-de
2025-12-26 17:09:01 +01:00
parent a119b4b5e9
commit c393fa55c8
7 changed files with 132 additions and 40 deletions

View File

@@ -205,6 +205,8 @@ public class G2DRenderer implements Renderer {
bo.staticCleanupAfterFrame(); bo.staticCleanupAfterFrame();
} }
this.rendererData.getRenderingBoPool().releasePending();
// //
// Rendering cycle END // Rendering cycle END
// //

View File

@@ -26,9 +26,9 @@ public class G2DRendererControl implements RendererControl {
private RendererData rendererData; private RendererData rendererData;
private CoordI2 absoluteViewportDimension = ImmutableCoordI2.one(); private CoordI2 absoluteViewportDimension = RendererControl.REFERENCE_RESOLUTION;
private CoordI2 effectiveViewportDimension = ImmutableCoordI2.one(); private CoordI2 effectiveViewportDimension = RendererControl.REFERENCE_RESOLUTION;
private double scaleFactor = 1.0; private double scaleFactor = 1.0;

View File

@@ -31,7 +31,7 @@ public class RendererWorkingSetImpl implements RendererWorkingSet {
.next() .next()
.put(work.getId(), work); .put(work.getId(), work);
if (previous != null && previous != work) { if (previous != null && previous != work) {
this.rendererBoPool.release(previous); this.rendererBoPool.returnToPool(previous);
} }
} }
@@ -41,7 +41,7 @@ public class RendererWorkingSetImpl implements RendererWorkingSet {
for (final RenderingBo bo : work) { for (final RenderingBo bo : work) {
final var previous = nextMap.put(bo.getId(), bo); final var previous = nextMap.put(bo.getId(), bo);
if (previous != null && previous != bo) { if (previous != null && previous != bo) {
this.rendererBoPool.release(previous); this.rendererBoPool.returnToPool(previous);
} }
} }
} }
@@ -52,7 +52,7 @@ public class RendererWorkingSetImpl implements RendererWorkingSet {
.next() .next()
.remove(id); .remove(id);
if (previous != null) { if (previous != null) {
this.rendererBoPool.release(previous); this.rendererBoPool.returnToPool(previous);
} }
} }
@@ -108,7 +108,7 @@ public class RendererWorkingSetImpl implements RendererWorkingSet {
final var previousLiveSet = previousLiveRef.get(); final var previousLiveSet = previousLiveRef.get();
if (previousLiveSet != null) { if (previousLiveSet != null) {
if (clone) { if (clone) {
previousLiveSet.values().forEach(this.rendererBoPool::release); previousLiveSet.values().forEach(this.rendererBoPool::returnToPool);
} }
this.returnMapToPool(previousLiveSet); this.returnMapToPool(previousLiveSet);
} }
@@ -119,7 +119,7 @@ public class RendererWorkingSetImpl implements RendererWorkingSet {
@Override @Override
public void clearNext(final String producerId) { public void clearNext(final String producerId) {
final var next = this.getWorkingSetByProducer(producerId).next(); final var next = this.getWorkingSetByProducer(producerId).next();
next.values().forEach(this.rendererBoPool::release); next.values().forEach(this.rendererBoPool::returnToPool);
next.clear(); next.clear();
} }
@@ -127,8 +127,8 @@ public class RendererWorkingSetImpl implements RendererWorkingSet {
public void clear(final String producerId) { public void clear(final String producerId) {
final RendererWork rendererWork = this.workingSet.remove(producerId); final RendererWork rendererWork = this.workingSet.remove(producerId);
if (rendererWork != null) { if (rendererWork != null) {
rendererWork.live().values().forEach(this.rendererBoPool::release); rendererWork.live().values().forEach(this.rendererBoPool::returnToPool);
rendererWork.next().values().forEach(this.rendererBoPool::release); rendererWork.next().values().forEach(this.rendererBoPool::returnToPool);
} }
} }

View File

@@ -20,9 +20,14 @@ public interface RenderingBoPool {
<T extends RenderingBo> T copy(T original); <T extends RenderingBo> T copy(T original);
/** /**
* Releases a rendering business object back to the pool. This is usually managed by the renderer. * Queues a rendering business object for return back to the pool. Renderer will release the BO after the current rendering cycle is complete.
*/ */
void release(RenderingBo obj); void returnToPool(RenderingBo obj);
/**
* Releases all pending rendering business objects back to the pool. This is managed by the renderer.
*/
void releasePending();
void setRendererData(RendererData rendererData); void setRendererData(RendererData rendererData);
} }

View File

@@ -1,9 +1,13 @@
package de.extio.game_engine.renderer.work; package de.extio.game_engine.renderer.work;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Stack; import java.util.Stack;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import de.extio.game_engine.renderer.RendererData; import de.extio.game_engine.renderer.RendererData;
import de.extio.game_engine.renderer.model.RenderingBo; import de.extio.game_engine.renderer.model.RenderingBo;
@@ -14,6 +18,10 @@ public class RenderingBoPoolImpl implements RenderingBoPool {
private final Map<Class<? extends RenderingBo>, Stack<RenderingBo>> pool = new HashMap<>(); private final Map<Class<? extends RenderingBo>, Stack<RenderingBo>> pool = new HashMap<>();
private final BlockingQueue<RenderingBo> returnQueue = new ArrayBlockingQueue<>(1000);
private final Deque<RenderingBo> returnSink = new ArrayDeque<>(1000);
private final LastMapping lastMapping = new LastMapping(); private final LastMapping lastMapping = new LastMapping();
private final LastReverseMapping lastReverseMapping = new LastReverseMapping(); private final LastReverseMapping lastReverseMapping = new LastReverseMapping();
@@ -98,8 +106,16 @@ public class RenderingBoPoolImpl implements RenderingBoPool {
} }
@Override @Override
public synchronized void release(final RenderingBo obj) { public void returnToPool(final RenderingBo obj) {
this.returnQueue.offer(obj);
}
@Override
public synchronized void releasePending() {
try { try {
this.returnQueue.drainTo(this.returnSink);
RenderingBo obj;
while ((obj = this.returnSink.poll()) != null) {
obj.close(); obj.close();
final Class<? extends RenderingBo> clazz = obj.getClass(); final Class<? extends RenderingBo> clazz = obj.getClass();
@@ -121,6 +137,7 @@ public class RenderingBoPoolImpl implements RenderingBoPool {
pooled.push(obj); pooled.push(obj);
} }
}
catch (final Exception e) { catch (final Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }

View File

@@ -194,8 +194,8 @@ public class RendererWorkingSetImplTest {
final var uncommittedAfter = this.workingSet.getUncommittedWork(this.moduleA.getId()); final var uncommittedAfter = this.workingSet.getUncommittedWork(this.moduleA.getId());
assertTrue(uncommittedAfter.isEmpty()); assertTrue(uncommittedAfter.isEmpty());
verify(this.renderingBoPool, times(1)).release(bo1); verify(this.renderingBoPool, times(1)).returnToPool(bo1);
verify(this.renderingBoPool, times(1)).release(bo2); verify(this.renderingBoPool, times(1)).returnToPool(bo2);
} }
@Test @Test
@@ -240,8 +240,8 @@ public class RendererWorkingSetImplTest {
this.workingSet.put(this.moduleA.getId(), bo3); this.workingSet.put(this.moduleA.getId(), bo3);
this.workingSet.commit(this.moduleA.getId(), true); this.workingSet.commit(this.moduleA.getId(), true);
verify(this.renderingBoPool, times(1)).release(bo1); verify(this.renderingBoPool, times(1)).returnToPool(bo1);
verify(this.renderingBoPool, times(1)).release(bo2); verify(this.renderingBoPool, times(1)).returnToPool(bo2);
} }
@Test @Test

View File

@@ -1,6 +1,7 @@
package de.extio.game_engine.renderer.work; package de.extio.game_engine.renderer.work;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertSame;
@@ -89,7 +90,8 @@ public class RenderingBoPoolImplTest {
assertEquals("id1", acquired1.getId()); assertEquals("id1", acquired1.getId());
assertEquals(0, acquired1.getCloseCallCount()); assertEquals(0, acquired1.getCloseCallCount());
this.pool.release(acquired1); this.pool.returnToPool(acquired1);
this.pool.releasePending();
assertEquals(1, acquired1.getCloseCallCount()); assertEquals(1, acquired1.getCloseCallCount());
final TestRenderingBo acquired2 = this.pool.acquire("id2", TestRenderingBo.class); final TestRenderingBo acquired2 = this.pool.acquire("id2", TestRenderingBo.class);
@@ -106,10 +108,12 @@ public class RenderingBoPoolImplTest {
final TestRenderingBo bo = this.pool.acquire("id", TestRenderingBo.class); final TestRenderingBo bo = this.pool.acquire("id", TestRenderingBo.class);
assertEquals(0, bo.getCloseCallCount()); assertEquals(0, bo.getCloseCallCount());
this.pool.release(bo); this.pool.returnToPool(bo);
this.pool.releasePending();
assertEquals(1, bo.getCloseCallCount()); assertEquals(1, bo.getCloseCallCount());
this.pool.release(bo); this.pool.returnToPool(bo);
this.pool.releasePending();
assertEquals(2, bo.getCloseCallCount()); assertEquals(2, bo.getCloseCallCount());
} }
@@ -169,10 +173,12 @@ public class RenderingBoPoolImplTest {
assertEquals(0, second.getCloseCallCount()); assertEquals(0, second.getCloseCallCount());
assertNotSame(first, second); assertNotSame(first, second);
this.pool.release(first); this.pool.returnToPool(first);
this.pool.releasePending();
assertEquals(1, first.getCloseCallCount()); assertEquals(1, first.getCloseCallCount());
this.pool.release(second); this.pool.returnToPool(second);
this.pool.releasePending();
assertEquals(1, second.getCloseCallCount()); assertEquals(1, second.getCloseCallCount());
final TestRenderingBo reacquired = this.pool.acquire("id3", TestRenderingBo.class); final TestRenderingBo reacquired = this.pool.acquire("id3", TestRenderingBo.class);
@@ -182,6 +188,66 @@ public class RenderingBoPoolImplTest {
assertTrue(reacquired == first || reacquired == second); assertTrue(reacquired == first || reacquired == second);
} }
@Test
void testReturnToPoolWithoutReleasePending() {
final TestRenderingBo first = this.pool.acquire("id1", TestRenderingBo.class);
final int id1 = System.identityHashCode(first);
assertEquals(0, first.getCloseCallCount());
this.pool.returnToPool(first);
assertEquals(0, first.getCloseCallCount());
final TestRenderingBo second = this.pool.acquire("id2", TestRenderingBo.class);
final int id2 = System.identityHashCode(second);
assertNotSame(first, second);
assertNotEquals(id1, id2);
assertEquals("id2", second.getId());
}
@Test
void testBatchReturnToPoolThenReleasePending() {
final TestRenderingBo first = this.pool.acquire("id1", TestRenderingBo.class);
final TestRenderingBo second = this.pool.acquire("id2", TestRenderingBo.class);
final TestRenderingBo third = this.pool.acquire("id3", TestRenderingBo.class);
assertEquals(0, first.getCloseCallCount());
assertEquals(0, second.getCloseCallCount());
assertEquals(0, third.getCloseCallCount());
this.pool.returnToPool(first);
this.pool.returnToPool(second);
this.pool.returnToPool(third);
assertEquals(0, first.getCloseCallCount());
assertEquals(0, second.getCloseCallCount());
assertEquals(0, third.getCloseCallCount());
this.pool.releasePending();
assertEquals(1, first.getCloseCallCount());
assertEquals(1, second.getCloseCallCount());
assertEquals(1, third.getCloseCallCount());
final TestRenderingBo reacquired = this.pool.acquire("id4", TestRenderingBo.class);
assertTrue(reacquired == first || reacquired == second || reacquired == third);
}
@Test
void testMultipleReleasePendingCalls() {
final TestRenderingBo bo = this.pool.acquire("id1", TestRenderingBo.class);
this.pool.returnToPool(bo);
this.pool.releasePending();
assertEquals(1, bo.getCloseCallCount());
this.pool.releasePending();
assertEquals(1, bo.getCloseCallCount());
this.pool.releasePending();
assertEquals(1, bo.getCloseCallCount());
}
@Test @Test
void testConcurrentAccess() throws InterruptedException { void testConcurrentAccess() throws InterruptedException {
final int[] thread1Count = {0}; final int[] thread1Count = {0};
@@ -192,7 +258,7 @@ public class RenderingBoPoolImplTest {
final TestRenderingBo bo = this.pool.acquire("id-" + i, TestRenderingBo.class); final TestRenderingBo bo = this.pool.acquire("id-" + i, TestRenderingBo.class);
assertNotNull(bo); assertNotNull(bo);
assertNotNull(bo.getId()); assertNotNull(bo.getId());
this.pool.release(bo); this.pool.returnToPool(bo);
thread1Count[0]++; thread1Count[0]++;
} }
}); });
@@ -202,7 +268,7 @@ public class RenderingBoPoolImplTest {
final TestRenderingBo bo = this.pool.acquire("id-" + i, TestRenderingBo.class); final TestRenderingBo bo = this.pool.acquire("id-" + i, TestRenderingBo.class);
assertNotNull(bo); assertNotNull(bo);
assertNotNull(bo.getId()); assertNotNull(bo.getId());
this.pool.release(bo); this.pool.returnToPool(bo);
thread2Count[0]++; thread2Count[0]++;
} }
}); });
@@ -216,6 +282,8 @@ public class RenderingBoPoolImplTest {
assertEquals(10, thread1Count[0]); assertEquals(10, thread1Count[0]);
assertEquals(10, thread2Count[0]); assertEquals(10, thread2Count[0]);
this.pool.releasePending();
final TestRenderingBo bo = this.pool.acquire("final-id", TestRenderingBo.class); final TestRenderingBo bo = this.pool.acquire("final-id", TestRenderingBo.class);
assertNotNull(bo); assertNotNull(bo);
assertEquals("final-id", bo.getId()); assertEquals("final-id", bo.getId());