Refactor rendering business object management and improve rendering pipeline

- Introduced RendererWorkingSet and RendererWorkingSetImpl to manage rendering business objects (BOs) more effectively.
- Updated RenderingBoPool to support acquiring and releasing BOs with unique IDs.
- Enhanced G2DRenderer to utilize the new RendererWorkingSet for managing rendering operations.
- Modified ControlRenderingBo and its implementations to use control IDs instead of generic IDs for better identification.
- Improved G2DAbstractRenderingBo and its subclasses to streamline rendering logic and resource management.
- Added unit tests for RendererWorkingSetImpl to ensure correct behavior of adding, committing, and retrieving rendering BOs.
- Removed unnecessary methods and cleaned up code for better readability and maintainability.
This commit is contained in:
extio-de
2025-12-22 22:56:55 +01:00
parent a971033d97
commit 4f77b0a23a
23 changed files with 659 additions and 96 deletions

View File

@@ -20,6 +20,9 @@ public class ModuleExecutorImpl implements ModuleExecutor {
@Override
public void execute() {
// TODO Subscriptions for modules to run only on certain conditions
this.runTasks(module -> () -> {
if (module instanceof final AbstractClientModule clientModule && this.moduleManager.getModulesDisplayedClientModules().contains(clientModule)) {
this.invokeSafe(clientModule, AbstractClientModule::runUiPre);

View File

@@ -1,14 +1,10 @@
package de.extio.game_engine.renderer;
import java.util.List;
import de.extio.game_engine.renderer.model.RenderingBo;
public interface Renderer {
void show();
void run(List<RenderingBo> renderingBOs) throws InterruptedException;
void run() throws InterruptedException;
void reset();

View File

@@ -15,6 +15,10 @@ import org.springframework.context.annotation.Bean;
import de.extio.game_engine.event.EventService;
import de.extio.game_engine.keyboard.KeycodeRegistry;
import de.extio.game_engine.renderer.model.RenderingBo;
import de.extio.game_engine.renderer.work.RendererWorkingSet;
import de.extio.game_engine.renderer.work.RendererWorkingSetImpl;
import de.extio.game_engine.renderer.work.RenderingBoPool;
import de.extio.game_engine.renderer.work.RenderingBoPoolImpl;
import de.extio.game_engine.storage.StorageService;
@AutoConfiguration
@@ -37,6 +41,12 @@ public class RendererAutoConfiguration {
return new RenderingBoPoolImpl(mapping);
}
@Bean
@ConditionalOnMissingBean
RendererWorkingSet rendererWorkingSet(final RenderingBoPool rendererBoPool) {
return new RendererWorkingSetImpl(rendererBoPool);
}
@Bean
RendererData rendererData(final ApplicationContext applicationContext,
final Renderer renderer,
@@ -44,9 +54,10 @@ public class RendererAutoConfiguration {
final RenderingBoPool renderingBoPool,
final KeycodeRegistry keycodeRegistry,
final EventService eventService,
final StorageService storageService) {
final StorageService storageService,
final RendererWorkingSet rendererWorkingSet) {
final var rendererData = new RendererData(applicationContext, renderer, rendererControl, renderingBoPool, keycodeRegistry, eventService, storageService);
final var rendererData = new RendererData(applicationContext, renderer, rendererControl, renderingBoPool, keycodeRegistry, eventService, storageService, rendererWorkingSet);
renderer.setRendererData(rendererData);
rendererControl.setRendererData(rendererData);
renderingBoPool.setRendererData(rendererData);

View File

@@ -9,6 +9,8 @@ import de.extio.game_engine.event.EventService;
import de.extio.game_engine.keyboard.KeycodeRegistry;
import de.extio.game_engine.renderer.model.options.UiOptions;
import de.extio.game_engine.renderer.model.options.VideoOptions;
import de.extio.game_engine.renderer.work.RendererWorkingSet;
import de.extio.game_engine.renderer.work.RenderingBoPool;
import de.extio.game_engine.storage.StorageService;
public class RendererData {
@@ -33,9 +35,11 @@ public class RendererData {
private final StorageService storageService;
private final RendererWorkingSet rendererWorkingSet;
private RendererLoop rendererLoop;
public RendererData(final ApplicationContext applicationContext, final Renderer renderer, final RendererControl rendererControl, final RenderingBoPool renderingBoPool, final KeycodeRegistry keycodeRegistry, final EventService eventService, final StorageService storageService) {
public RendererData(final ApplicationContext applicationContext, final Renderer renderer, final RendererControl rendererControl, final RenderingBoPool renderingBoPool, final KeycodeRegistry keycodeRegistry, final EventService eventService, final StorageService storageService, final RendererWorkingSet rendererWorkingSet) {
this.applicationContext = applicationContext;
this.renderer = renderer;
this.rendererControl = rendererControl;
@@ -43,6 +47,7 @@ public class RendererData {
this.keycodeRegistry = keycodeRegistry;
this.eventService = eventService;
this.storageService = storageService;
this.rendererWorkingSet = rendererWorkingSet;
}
public ApplicationContext getApplicationContext() {
@@ -96,4 +101,8 @@ public class RendererData {
public StorageService getStorageService() {
return storageService;
}
public RendererWorkingSet getRendererWorkingSet() {
return rendererWorkingSet;
}
}

View File

@@ -1,7 +1,6 @@
package de.extio.game_engine.renderer;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
@@ -41,7 +40,8 @@ public class RendererLoop implements Runnable {
while (!this.shutdown) {
try {
this.rendererData.nextFrame();
this.rendererData.getRenderer().run(new ArrayList<>()); // TODO: Pass actual renderables
// TODO run modules here
this.rendererData.getRenderer().run();
exceptionCount = 0;
}
catch (final InterruptedException e) {

View File

@@ -3,7 +3,7 @@ package de.extio.game_engine.renderer.container;
import java.util.function.Function;
import de.extio.game_engine.renderer.RendererControl;
import de.extio.game_engine.renderer.RenderingBoPool;
import de.extio.game_engine.renderer.work.RenderingBoPool;
import de.extio.game_engine.spatial2.model.Area2;
/**

View File

@@ -6,13 +6,13 @@ import java.util.function.Predicate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.extio.game_engine.renderer.RenderingBoPool;
import de.extio.game_engine.renderer.model.RenderingBo;
import de.extio.game_engine.renderer.model.RenderingBoLayer;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.SliderControl;
import de.extio.game_engine.renderer.model.color.RgbaColor;
import de.extio.game_engine.renderer.model.event.UiControlEvent;
import de.extio.game_engine.renderer.work.RenderingBoPool;
import de.extio.game_engine.spatial2.model.Area2;
import de.extio.game_engine.spatial2.model.CoordI2;
@@ -57,8 +57,8 @@ public class ScrollBar {
public void render(final List<RenderingBo> renderingBo, final Area2 renderingArea, final int numElements, final RenderingBoPool renderingBoPool) {
this.lastNumElements = numElements;
final var bo = renderingBoPool.acquire(ControlRenderingBo.class)
.setId(this.name)
final var bo = renderingBoPool.acquire(this.name + "_Slider", ControlRenderingBo.class)
.setControlId(this.name)
.setType(SliderControl.class)
.setCustomData(Boolean.FALSE)
.setCustomData2(Double.valueOf(1.0D - this.scrollPosition))

View File

@@ -9,12 +9,12 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.extio.game_engine.renderer.RendererControl;
import de.extio.game_engine.renderer.RenderingBoPool;
import de.extio.game_engine.renderer.model.RenderingBo;
import de.extio.game_engine.renderer.model.RenderingBoLayer;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.WindowCloseButtonControl;
import de.extio.game_engine.renderer.model.color.RgbaColor;
import de.extio.game_engine.renderer.work.RenderingBoPool;
import de.extio.game_engine.renderer.model.bo.DrawWindowRenderingBo;
import de.extio.game_engine.spatial2.WorldUtils2;
import de.extio.game_engine.spatial2.model.Area2;
@@ -173,7 +173,7 @@ public class Window implements HasPositionAndDimension2, AutoCloseable {
this.updateArea();
}
var bo = this.renderingBoPool.acquire(DrawWindowRenderingBo.class)
var bo = this.renderingBoPool.acquire(this.name + "_Window", DrawWindowRenderingBo.class)
.setThickBorder(this.draggable)
.withDimensionAbsolute(this.area.getDimension())
.withPositionAbsoluteAnchorTopLeft(this.area.getPosition())
@@ -184,9 +184,9 @@ public class Window implements HasPositionAndDimension2, AutoCloseable {
consumer.accept(renderingBo, bo);
if (this.drawCloseButton) {
bo = this.renderingBoPool.acquire(ControlRenderingBo.class)
bo = this.renderingBoPool.acquire(this.name + "_Close", ControlRenderingBo.class)
.setType(WindowCloseButtonControl.class)
.setId(this.name + "_Close")
.setControlId(this.name + "_Close")
.setCustomData(this.draggable)
.setEnabled(true)
.setVisible(true)

View File

@@ -8,9 +8,8 @@ import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.HashSet;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@@ -25,7 +24,6 @@ import de.extio.game_engine.renderer.RendererData;
import de.extio.game_engine.renderer.g2d.bo.rendering.G2DAbstractRenderingBo;
import de.extio.game_engine.renderer.g2d.bo.rendering.G2DDrawBackground;
import de.extio.game_engine.renderer.g2d.bo.rendering.G2DDrawFpsHistory;
import de.extio.game_engine.renderer.g2d.bo.rendering.G2DDrawImage;
import de.extio.game_engine.renderer.g2d.control.G2DDrawControl;
import de.extio.game_engine.renderer.g2d.control.G2DDrawControlTooltip;
import de.extio.game_engine.renderer.model.RenderingBo;
@@ -43,10 +41,12 @@ public class G2DRenderer implements Renderer {
private final RingBuffer<Integer> fpsHistory = new RingBuffer<>(90);
private final Set<Class<? extends RenderingBo>> releasedBoClasses = new HashSet<>();
private final StringBuilder fpsStringBuilder = new StringBuilder(15);
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(true);
private final List<RenderingBo> renderingBOs = new ArrayList<>();
private volatile G2DMainFrame mainFrame;
private RendererData rendererData;
@@ -61,8 +61,6 @@ public class G2DRenderer implements Renderer {
private long frameDur;
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(true);
private String title;
private volatile boolean takeScreenshot;
@@ -125,7 +123,7 @@ public class G2DRenderer implements Renderer {
}
@Override
public void run(final List<RenderingBo> renderingBOs) throws InterruptedException {
public void run() throws InterruptedException {
try {
this.rwLock.readLock().lockInterruptibly();
try {
@@ -150,8 +148,9 @@ public class G2DRenderer implements Renderer {
screenshotGraphics.setRenderingHints(G2DRenderingHintFactory.createDefault());
}
this.addStaticRenderingBOs(renderingBOs);
renderingBOs.sort((bo0, bo1) -> bo0.getLayer().compareTo(bo1.getLayer()));
this.addStaticRenderingBOs();
this.rendererData.getRendererWorkingSet().getLiveSet(this.renderingBOs);
this.renderingBOs.sort((bo0, bo1) -> bo0.getLayer().compareTo(bo1.getLayer()));
Graphics2D screenGraphics = null;
try {
@@ -166,11 +165,11 @@ public class G2DRenderer implements Renderer {
screenGraphics.setColor(Color.BLACK);
screenGraphics.fillRect(0, 0, viewPortDimension.getX(), viewPortDimension.getY());
for (final RenderingBo renderingBO : renderingBOs) {
if (renderingBO instanceof G2DAbstractRenderingBo) {
((G2DAbstractRenderingBo) renderingBO).render(screenGraphics, this.rendererData.getRendererControl().getScaleFactor(), false);
if (takeScreenshot_ && ((G2DAbstractRenderingBo) renderingBO).isScreenshotRelevant()) {
((G2DAbstractRenderingBo) renderingBO).render(screenshotGraphics, this.rendererData.getRendererControl().getScaleFactor(), true);
for (final RenderingBo renderingBO : this.renderingBOs) {
if (renderingBO instanceof final G2DAbstractRenderingBo g2dAbstractRenderingBo) {
g2dAbstractRenderingBo.render(screenGraphics, this.rendererData.getRendererControl().getScaleFactor(), false);
if (takeScreenshot_ && g2dAbstractRenderingBo.isScreenshotRelevant()) {
g2dAbstractRenderingBo.render(screenshotGraphics, this.rendererData.getRendererControl().getScaleFactor(), true);
}
}
}
@@ -192,14 +191,6 @@ public class G2DRenderer implements Renderer {
if (screenGraphics != null) {
screenGraphics.dispose();
}
this.releasedBoClasses.clear();
for (final RenderingBo renderingBO : renderingBOs) {
if (this.releasedBoClasses.add(renderingBO.getClass()) && renderingBO instanceof final G2DAbstractRenderingBo g2dRenderingBo) {
g2dRenderingBo.closeStatic();
}
this.rendererData.getRenderingBoPool().release(renderingBO);
}
}
if (takeScreenshot_) {
@@ -207,6 +198,7 @@ public class G2DRenderer implements Renderer {
}
}
finally {
this.renderingBOs.clear();
this.rwLock.readLock().unlock();
}
}
@@ -217,13 +209,14 @@ public class G2DRenderer implements Renderer {
}
}
private void addStaticRenderingBOs(final List<RenderingBo> renderingBOs) {
renderingBOs.add(this.rendererData.getRenderingBoPool().acquire(G2DDrawBackground.class));
renderingBOs.add(this.rendererData.getRenderingBoPool().acquire(G2DDrawControlTooltip.class));
this.drawStatistics(renderingBOs);
private void addStaticRenderingBOs() {
this.rendererData.getRendererWorkingSet().add("G2DRenderer", this.rendererData.getRenderingBoPool().acquire("G2DRenderer_background", G2DDrawBackground.class));
this.rendererData.getRendererWorkingSet().add("G2DRenderer", this.rendererData.getRenderingBoPool().acquire("G2DRenderer_control", G2DDrawControlTooltip.class));
this.drawStatistics();
this.rendererData.getRendererWorkingSet().commit("G2DRenderer", false);
}
private void drawStatistics(final List<RenderingBo> renderingBOs) {
private void drawStatistics() {
if (!this.rendererData.getUiOptions().isDrawFps()) {
return;
}
@@ -236,20 +229,20 @@ public class G2DRenderer implements Renderer {
fpsStringBuilder.append(this.frameDur);
fpsStringBuilder.append("ms");
final var drawFont = this.rendererData.getRenderingBoPool().acquire(DrawFontRenderingBo.class)
final var drawFont = this.rendererData.getRenderingBoPool().acquire("g2DRenderer_fpsText", DrawFontRenderingBo.class)
.setText(fpsStringBuilder.toString())
.setSize(14)
.setColor(RgbaColor.WHITE)
.setLayer(RenderingBoLayer.TOP)
.withPositionAbsoluteAnchorTopRight(100, 35);
renderingBOs.add(drawFont);
this.rendererData.getRendererWorkingSet().add("G2DRenderer", drawFont);
this.fpsHistory.add(Integer.valueOf((int) this.frameDur));
final var drawFpsHistory = this.rendererData.getRenderingBoPool().acquire(G2DDrawFpsHistory.class)
final var drawFpsHistory = this.rendererData.getRenderingBoPool().acquire("g2DRenderer_fpsHistory", G2DDrawFpsHistory.class)
.setHistory(this.fpsHistory)
.setLayer(RenderingBoLayer.TOP)
.withPositionAbsoluteAnchorTopRight(100, 50);
renderingBOs.add(drawFpsHistory);
this.rendererData.getRendererWorkingSet().add("G2DRenderer", drawFpsHistory);
}
private void frameCap() throws InterruptedException {

View File

@@ -17,6 +17,7 @@ public abstract class G2DAbstractRenderingBo extends AbstractRenderingBo {
public abstract void render(Graphics2D graphics, double scaleFactor, boolean force);
@Override
public void closeStatic() {
}

View File

@@ -80,9 +80,6 @@ public class G2DDrawBackground extends G2DAbstractRenderingBo {
this.loadImage(graphics, index, backgroundName);
}
if (G2DDrawBackground.BACKGROUND_IMAGE == null) {
if (index == 0) {
this.fillBlack(graphics, viewPort);
}
return;
}
else if (BACKGROUND_IMAGE.validate(graphics.getDeviceConfiguration()) != VolatileImage.IMAGE_OK) {
@@ -162,11 +159,6 @@ public class G2DDrawBackground extends G2DAbstractRenderingBo {
}
}
private void fillBlack(final Graphics2D graphics, final CoordI2 viewPort) {
graphics.setColor(Color.BLACK);
graphics.fillRect(0, 0, viewPort.getX(), viewPort.getY());
}
private void loadImage(final Graphics2D graphics, final int index, final String backgroundName) {
if (G2DDrawBackground.BACKGROUND_IMAGE != null) {
G2DDrawBackground.BACKGROUND_IMAGE.flush();

View File

@@ -39,7 +39,7 @@ public class G2DDrawControl extends G2DAbstractRenderingBo implements ControlRen
public static Map<String, List<BaseControl>> CONTROL_GROUPS = new HashMap<>();
private String id;
private String controlId;
private Class<? extends BaseControl> clazz;
@@ -68,14 +68,14 @@ public class G2DDrawControl extends G2DAbstractRenderingBo implements ControlRen
}
@Override
public ControlRenderingBo setId(final String id) {
this.id = id;
public ControlRenderingBo setControlId(final String id) {
this.controlId = id;
return this;
}
@Override
public String getId() {
return this.id;
public String getControlId() {
return this.controlId;
}
@Override
@@ -146,7 +146,7 @@ public class G2DDrawControl extends G2DAbstractRenderingBo implements ControlRen
@Override
public void render(final Graphics2D graphics, final double scaleFactor, final boolean force) {
var control = (G2DBaseControlImpl) CACHED_CONTROLS.get(this.id);
var control = (G2DBaseControlImpl) CACHED_CONTROLS.get(this.controlId);
if (control == null) {
if (this.clazz == ButtonControl.class) {
control = new G2DButtonControlImpl();
@@ -189,7 +189,7 @@ public class G2DDrawControl extends G2DAbstractRenderingBo implements ControlRen
control.setScaleFactor(scaleFactor)
.setControlGroup(this.controlGroup)
.setRendererData(this.rendererData)
.setId(this.id);
.setControlId(this.controlId);
if (this.clazz == TextfieldControl.class && this.customData instanceof Boolean) {
((TextfieldControl) control).setMultiLine(((Boolean) this.customData).booleanValue());
}
@@ -197,7 +197,7 @@ public class G2DDrawControl extends G2DAbstractRenderingBo implements ControlRen
control.build();
CACHED_CONTROLS.put(this.id, control);
CACHED_CONTROLS.put(this.controlId, control);
if (this.controlGroup != null && !this.controlGroup.isEmpty()) {
var controls = CONTROL_GROUPS.get(this.controlGroup);
if (controls == null) {
@@ -315,7 +315,7 @@ public class G2DDrawControl extends G2DAbstractRenderingBo implements ControlRen
public void close() throws Exception {
super.close();
this.id = null;
this.controlId = null;
this.clazz = null;
this.caption = null;
this.controlGroup = null;

View File

@@ -72,7 +72,7 @@ public abstract class G2DBaseControlImpl implements BaseControl {
}
@Override
public BaseControl setId(final String id) {
public BaseControl setControlId(final String id) {
this.id = id;
return this;
}

View File

@@ -234,9 +234,9 @@ public class G2DTableControlImpl extends G2DBaseControlImpl implements TableCont
}
@Override
public BaseControl setId(final String id) {
this.scrollbar.setId(id + "_scroll");
return super.setId(id);
public BaseControl setControlId(final String id) {
this.scrollbar.setControlId(id + "_scroll");
return super.setControlId(id);
}
@Override

View File

@@ -353,9 +353,9 @@ public class G2DTextfieldControlImpl2 extends G2DBaseControlImpl implements Text
}
@Override
public BaseControl setId(final String id) {
this.scrollbar.setId(id + "_scroll");
return super.setId(id);
public BaseControl setControlId(final String id) {
this.scrollbar.setControlId(id + "_scroll");
return super.setControlId(id);
}
}

View File

@@ -11,6 +11,8 @@ public abstract class AbstractRenderingBo implements RenderingBo {
protected final static Logger LOGGER = LoggerFactory.getLogger(AbstractRenderingBo.class);
protected String id;
protected RendererData rendererData;
protected RgbaColor color;
@@ -36,6 +38,16 @@ public abstract class AbstractRenderingBo implements RenderingBo {
this.defaultLayer = layer;
}
@Override
public void setId(final String id) {
this.id = id;
}
@Override
public String getId() {
return this.id;
}
@Override
public RenderingBo withPositionRelative(final int x, final int y) {
this.localX = x;
@@ -203,6 +215,7 @@ public abstract class AbstractRenderingBo implements RenderingBo {
@Override
public void close() throws Exception {
this.id = null;
this.color = null;
this.x = 0;
this.y = 0;
@@ -213,6 +226,11 @@ public abstract class AbstractRenderingBo implements RenderingBo {
this.layer = this.defaultLayer;
}
@Override
public void closeStatic() {
}
@Override
public void setRendererData(final RendererData RendererData) {
this.rendererData = (RendererData) RendererData;

View File

@@ -11,7 +11,9 @@ import de.extio.game_engine.spatial2.model.HasPosition2;
*/
public interface RenderingBo extends AutoCloseable {
void setRendererData(RendererData RendererData);
void setId(String id);
String getId();
/**
* Sets a color you want to use for rendering. It depends on the rendering bo how this information is treated.
@@ -105,4 +107,8 @@ public interface RenderingBo extends AutoCloseable {
*/
RenderingBo withPositionRelative(HasPosition2 position);
void closeStatic();
void setRendererData(RendererData RendererData);
}

View File

@@ -11,9 +11,9 @@ import de.extio.game_engine.renderer.model.color.RgbaColor;
*/
public interface ControlRenderingBo extends RenderingBoHasDimension {
ControlRenderingBo setId(String id);
ControlRenderingBo setControlId(String id);
String getId();
String getControlId();
/**
* Sets the type of the control. See class tree of BaseControl
@@ -63,7 +63,7 @@ public interface ControlRenderingBo extends RenderingBoHasDimension {
boolean getInUse();
BaseControl setId(String id);
BaseControl setControlId(String id);
BaseControl setCaption(String caption);

View File

@@ -0,0 +1,22 @@
package de.extio.game_engine.renderer.work;
import java.util.List;
import java.util.Map;
import de.extio.game_engine.renderer.model.RenderingBo;
public interface RendererWorkingSet {
void add(String producer, RenderingBo work);
void add(String producer, List<RenderingBo> work);
RenderingBo get(String producer, String id);
Map<String, RenderingBo> getUncommittedWork(String producer);
Map<String, RenderingBo> commit(String producer, boolean clone);
void getLiveSet(List<RenderingBo> combinedLiveSet);
}

View File

@@ -0,0 +1,129 @@
package de.extio.game_engine.renderer.work;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicReference;
import de.extio.game_engine.renderer.model.RenderingBo;
public class RendererWorkingSetImpl implements RendererWorkingSet {
private final ConcurrentMap<String, RendererWork> workingSet = new ConcurrentHashMap<>();
private final BlockingQueue<Map<String, RenderingBo>> mapsPool = new ArrayBlockingQueue<>(100);
private final BlockingQueue<Set<Class<? extends RenderingBo>>> setsPool = new ArrayBlockingQueue<>(100);
private final RenderingBoPool rendererBoPool;
public RendererWorkingSetImpl(final RenderingBoPool rendererBoPool) {
this.rendererBoPool = rendererBoPool;
}
@Override
public void add(final String producer, final RenderingBo work) {
this.getWorkingSetByProducer(producer)
.next()
.put(work.getId(), work);
}
@Override
public void add(final String producer, final List<RenderingBo> work) {
final var nextMap = this.getWorkingSetByProducer(producer).next();
for (final RenderingBo bo : work) {
nextMap.put(bo.getId(), bo);
}
}
@Override
public Map<String, RenderingBo> getUncommittedWork(final String producer) {
return this.getWorkingSetByProducer(producer).next();
}
@Override
public RenderingBo get(final String producer, final String id) {
return this.getWorkingSetByProducer(producer).next().get(id);
}
@Override
public Map<String, RenderingBo> commit(final String producer, final boolean clone) {
final AtomicReference<Map<String, RenderingBo>> previousWork = new AtomicReference<>();
final RendererWork rendererWork = this.workingSet.compute(producer, (k, v) -> {
if (v == null) {
return new RendererWork(this.obtainMapFromPool(), this.obtainMapFromPool());
}
previousWork.set(v.live());
if (clone) {
final Map<String, RenderingBo> newNext = this.obtainMapFromPool();
newNext.putAll(v.next());
return new RendererWork(v.next(), newNext);
}
else {
return new RendererWork(v.next(), this.obtainMapFromPool());
}
});
final var previousWorkMap = previousWork.get();
if (previousWorkMap != null) {
final var releasedBoClasses = this.obtainSetFromPool();
for (final RenderingBo renderingBO : previousWorkMap.values()) {
if (releasedBoClasses.add(renderingBO.getClass())) {
renderingBO.closeStatic();
}
this.rendererBoPool.release(renderingBO);
}
this.returnMapToPool(previousWorkMap);
this.returnSetToPool(releasedBoClasses);
}
return rendererWork.next();
}
@Override
public void getLiveSet(final List<RenderingBo> combinedLiveSet) {
this.workingSet.values().forEach(rendererWork -> combinedLiveSet.addAll(rendererWork.live().values()));
}
private RendererWork getWorkingSetByProducer(final String producer) {
return this.workingSet.computeIfAbsent(producer, k -> new RendererWork(this.obtainMapFromPool(), this.obtainMapFromPool()));
}
private Map<String, RenderingBo> obtainMapFromPool() {
var map = this.mapsPool.poll();
if (map == null) {
map = Collections.synchronizedMap(new HashMap<>());
}
return map;
}
private void returnMapToPool(final Map<String, RenderingBo> map) {
map.clear();
this.mapsPool.offer(map);
}
private Set<Class<? extends RenderingBo>> obtainSetFromPool() {
var set = this.setsPool.poll();
if (set == null) {
set = new HashSet<>();
}
return set;
}
private void returnSetToPool(final Set<Class<? extends RenderingBo>> set) {
set.clear();
this.setsPool.offer(set);
}
private static record RendererWork(Map<String, RenderingBo> live, Map<String, RenderingBo> next) {
}
}

View File

@@ -1,5 +1,6 @@
package de.extio.game_engine.renderer;
package de.extio.game_engine.renderer.work;
import de.extio.game_engine.renderer.RendererData;
import de.extio.game_engine.renderer.model.RenderingBo;
/**
@@ -14,7 +15,7 @@ public interface RenderingBoPool {
/**
* Acquires a rendering business object by type from the pool.
*/
<T extends RenderingBo> T acquire(Class<T> clazz);
<T extends RenderingBo> T acquire(String id, Class<T> clazz);
/**
* Releases a rendering business object back to the pool. This is usually managed by the renderer.

View File

@@ -1,10 +1,11 @@
package de.extio.game_engine.renderer;
package de.extio.game_engine.renderer.work;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
import de.extio.game_engine.renderer.RendererData;
import de.extio.game_engine.renderer.model.RenderingBo;
public class RenderingBoPoolImpl implements RenderingBoPool {
@@ -41,7 +42,7 @@ public class RenderingBoPoolImpl implements RenderingBoPool {
@Override
@SuppressWarnings("unchecked")
public <T extends RenderingBo> T acquire(final Class<T> clazz) {
public synchronized <T extends RenderingBo> T acquire(final String id, final Class<T> clazz) {
Stack<RenderingBo> pooled;
Class<? extends RenderingBo> impl;
@@ -71,8 +72,8 @@ public class RenderingBoPoolImpl implements RenderingBoPool {
this.lastMapping.pooled = pooled;
}
T instance;
if (pooled.isEmpty()) {
T instance;
try {
instance = clazz.cast(impl.getDeclaredConstructor().newInstance());
}
@@ -80,14 +81,17 @@ public class RenderingBoPoolImpl implements RenderingBoPool {
throw new RuntimeException(e);
}
instance.setRendererData(this.rendererData);
return instance;
}
else {
instance = (T) pooled.pop();
}
return (T) pooled.pop();
instance.setId(id);
return instance;
}
@Override
public void release(final RenderingBo obj) {
public synchronized void release(final RenderingBo obj) {
try {
obj.close();

View File

@@ -0,0 +1,378 @@
package de.extio.game_engine.renderer.work;
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;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import de.extio.game_engine.renderer.RendererData;
import de.extio.game_engine.renderer.model.RenderingBo;
import de.extio.game_engine.renderer.model.RenderingBoLayer;
import de.extio.game_engine.renderer.model.color.RgbaColor;
import de.extio.game_engine.spatial2.model.HasPosition2;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class RendererWorkingSetImplTest {
private RendererWorkingSetImpl workingSet;
private RenderingBoPool renderingBoPool;
@BeforeEach
void setUp() {
this.renderingBoPool = mock(RenderingBoPool.class);
this.workingSet = new RendererWorkingSetImpl(this.renderingBoPool);
}
@Test
void addSingleAddsToUncommittedWork() {
final RenderingBo bo1 = mock(RenderingBo.class);
when(bo1.getId()).thenReturn("bo1");
this.workingSet.add("producer", bo1);
final var uncommitted = this.workingSet.getUncommittedWork("producer");
assertNotNull(uncommitted);
assertEquals(Map.of("bo1", bo1), uncommitted);
}
@Test
void addListAddsAllToUncommittedWork() {
final RenderingBo bo1 = mock(RenderingBo.class);
when(bo1.getId()).thenReturn("bo1");
final RenderingBo bo2 = mock(RenderingBo.class);
when(bo2.getId()).thenReturn("bo2");
this.workingSet.add("producer", List.of(bo1, bo2));
final var uncommitted = this.workingSet.getUncommittedWork("producer");
assertEquals(Map.of("bo1", bo1, "bo2", bo2), uncommitted);
}
@Test
void commitWithoutCloneMovesUncommittedToLiveAndClearsUncommitted() {
final RenderingBo bo1 = mock(RenderingBo.class);
when(bo1.getId()).thenReturn("bo1");
final RenderingBo bo2 = mock(RenderingBo.class);
when(bo2.getId()).thenReturn("bo2");
this.workingSet.add("producer", bo1);
this.workingSet.add("producer", bo2);
final var returnedUncommitted = this.workingSet.commit("producer", false);
final var currentUncommitted = this.workingSet.getUncommittedWork("producer");
assertSame(currentUncommitted, returnedUncommitted);
assertTrue(currentUncommitted.isEmpty());
final List<RenderingBo> live = new ArrayList<>();
this.workingSet.getLiveSet(live);
assertEquals(2, live.size());
assertTrue(live.containsAll(List.of(bo1, bo2)));
}
@Test
void commitWithCloneCopiesUncommittedIntoNewUncommitted() {
final RenderingBo bo1 = mock(RenderingBo.class);
when(bo1.getId()).thenReturn("bo1");
final RenderingBo bo2 = mock(RenderingBo.class);
when(bo2.getId()).thenReturn("bo2");
final RenderingBo bo3 = mock(RenderingBo.class);
when(bo3.getId()).thenReturn("bo3");
this.workingSet.add("producer", bo1);
this.workingSet.add("producer", bo2);
final var returnedUncommitted = this.workingSet.commit("producer", true);
final var currentUncommitted = this.workingSet.getUncommittedWork("producer");
assertSame(currentUncommitted, returnedUncommitted);
assertEquals(Map.of("bo1", bo1, "bo2", bo2), currentUncommitted);
final List<RenderingBo> live = new ArrayList<>();
this.workingSet.getLiveSet(live);
assertEquals(2, live.size());
assertTrue(live.containsAll(List.of(bo1, bo2)));
this.workingSet.add("producer", bo3);
final List<RenderingBo> liveAfterAdd = new ArrayList<>();
this.workingSet.getLiveSet(liveAfterAdd);
assertEquals(2, liveAfterAdd.size());
assertTrue(liveAfterAdd.containsAll(List.of(bo1, bo2)));
}
@Test
void liveSetContainsAllProducersCommittedWork() {
final RenderingBo bo1 = mock(RenderingBo.class);
when(bo1.getId()).thenReturn("bo1");
final RenderingBo bo2 = mock(RenderingBo.class);
when(bo2.getId()).thenReturn("bo2");
final RenderingBo bo3 = mock(RenderingBo.class);
when(bo3.getId()).thenReturn("bo3");
this.workingSet.add("p1", bo1);
this.workingSet.add("p1", bo2);
this.workingSet.add("p2", bo3);
this.workingSet.commit("p1", false);
this.workingSet.commit("p2", false);
final List<RenderingBo> live = new ArrayList<>();
this.workingSet.getLiveSet(live);
assertEquals(3, live.size());
assertTrue(live.containsAll(List.of(bo1, bo2, bo3)));
}
@Test
void commitOnUnknownProducerCreatesEmptyState() {
final var returnedUncommitted = this.workingSet.commit("unknown", false);
assertNotNull(returnedUncommitted);
assertTrue(returnedUncommitted.isEmpty());
final List<RenderingBo> live = new ArrayList<>();
this.workingSet.getLiveSet(live);
assertTrue(live.isEmpty());
}
@Test
void secondCommitReleasesPreviousLiveWorkAndCallsCloseStaticOncePerClass() {
final var bo1 = new TestBoA();
bo1.setId("bo1");
final var bo2 = new TestBoA();
bo2.setId("bo2");
this.workingSet.add("producer", bo1);
this.workingSet.add("producer", bo2);
this.workingSet.commit("producer", false);
final var bo3 = new TestBoB();
bo3.setId("bo3");
this.workingSet.add("producer", bo3);
this.workingSet.commit("producer", false);
verify(this.renderingBoPool, times(1)).release(bo1);
verify(this.renderingBoPool, times(1)).release(bo2);
// closeStatic() is called exactly once per class, so one of bo1 or bo2 has it called
final int totalCloseStaticCallsForTestBoA = bo1.closeStaticCalls() + bo2.closeStaticCalls();
assertEquals(1, totalCloseStaticCallsForTestBoA);
assertEquals(0, bo3.closeStaticCalls());
}
@Test
void getReturnsRenderingBoFromUncommittedWork() {
final RenderingBo bo1 = mock(RenderingBo.class);
when(bo1.getId()).thenReturn("bo1");
final RenderingBo bo2 = mock(RenderingBo.class);
when(bo2.getId()).thenReturn("bo2");
this.workingSet.add("producer", bo1);
this.workingSet.add("producer", bo2);
final var retrieved1 = this.workingSet.get("producer", "bo1");
final var retrieved2 = this.workingSet.get("producer", "bo2");
final var notFound = this.workingSet.get("producer", "nonexistent");
assertSame(bo1, retrieved1);
assertSame(bo2, retrieved2);
assertTrue(notFound == null);
}
@Test
void getRetrievesFromUncommittedWorkOnly() {
final RenderingBo bo1 = mock(RenderingBo.class);
when(bo1.getId()).thenReturn("bo1");
final RenderingBo bo2 = mock(RenderingBo.class);
when(bo2.getId()).thenReturn("bo2");
this.workingSet.add("producer", bo1);
this.workingSet.add("producer", bo2);
final var committedWork = this.workingSet.commit("producer", false);
// After commit without clone, the uncommitted work should be empty
assertTrue(committedWork.isEmpty());
// get() only retrieves from next set, not live set, so it returns null after commit
final var retrieved1 = this.workingSet.get("producer", "bo1");
final var retrieved2 = this.workingSet.get("producer", "bo2");
assertTrue(retrieved1 == null);
assertTrue(retrieved2 == null);
}
@Test
void addWithSameIdReplacesExistingRenderingBo() {
final RenderingBo bo1 = mock(RenderingBo.class);
when(bo1.getId()).thenReturn("bo1");
final RenderingBo bo1_replacement = mock(RenderingBo.class);
when(bo1_replacement.getId()).thenReturn("bo1");
this.workingSet.add("producer", bo1);
final var uncommittedBefore = this.workingSet.getUncommittedWork("producer");
assertEquals(1, uncommittedBefore.size());
assertSame(bo1, uncommittedBefore.get("bo1"));
this.workingSet.add("producer", bo1_replacement);
final var uncommittedAfter = this.workingSet.getUncommittedWork("producer");
assertEquals(1, uncommittedAfter.size());
assertSame(bo1_replacement, uncommittedAfter.get("bo1"));
}
private static abstract class TestBoBase implements RenderingBo {
private final AtomicInteger closeStaticCalls = new AtomicInteger();
private String id;
@Override
public void setId(final String id) {
this.id = id;
}
@Override
public String getId() {
return this.id;
}
@Override
public RenderingBo setColor(final RgbaColor color) {
return this;
}
@Override
public int getX() {
return 0;
}
@Override
public int getY() {
return 0;
}
@Override
public int getLocalX() {
return 0;
}
@Override
public int getLocalY() {
return 0;
}
@Override
public RenderingBo setLayer(final RenderingBoLayer layer) {
return this;
}
@Override
public RenderingBoLayer getLayer() {
return RenderingBoLayer.UI0_0;
}
@Override
public RenderingBo withPositionAbsoluteAnchorTopLeft(final int x, final int y) {
return this;
}
@Override
public RenderingBo withPositionAbsoluteAnchorTopLeft(final HasPosition2 position) {
return this;
}
@Override
public RenderingBo withPositionAbsoluteAnchorTopRight(final int x, final int y) {
return this;
}
@Override
public RenderingBo withPositionAbsoluteAnchorTopRight(final HasPosition2 position) {
return this;
}
@Override
public RenderingBo withPositionAbsoluteAnchorBottomLeft(final int x, final int y) {
return this;
}
@Override
public RenderingBo withPositionAbsoluteAnchorBottomLeft(final HasPosition2 position) {
return this;
}
@Override
public RenderingBo withPositionAbsoluteAnchorBottomRight(final int x, final int y) {
return this;
}
@Override
public RenderingBo withPositionAbsoluteAnchorBottomRight(final HasPosition2 position) {
return this;
}
@Override
public RenderingBo withPositionPercentual(final double x, final double y) {
return this;
}
@Override
public RenderingBo withPositionIncrementalAbsolute(final int x, final int y) {
return this;
}
@Override
public RenderingBo withPositionIncrementalAbsolute(final HasPosition2 position) {
return this;
}
@Override
public RenderingBo withPositionIncrementalPercentual(final double x, final double y) {
return this;
}
@Override
public RenderingBo withPositionRelative(final int x, final int y) {
return this;
}
@Override
public RenderingBo withPositionRelative(final HasPosition2 position) {
return this;
}
@Override
public void closeStatic() {
this.closeStaticCalls.incrementAndGet();
}
public int closeStaticCalls() {
return this.closeStaticCalls.get();
}
@Override
public void setRendererData(final RendererData RendererData) {
}
@Override
public void close() {
}
}
private static final class TestBoA extends TestBoBase {
}
private static final class TestBoB extends TestBoBase {
}
}