mirror of
https://github.com/bancika/diy-layout-creator
synced 2026-11-10 09:12:09 +10:00
give the cache a chance to prepare all the components in multiple
threads
This commit is contained in:
@@ -31,6 +31,7 @@ import org.diylc.common.ComponentType;
|
||||
import org.diylc.core.ComponentState;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
import org.diylc.core.Project;
|
||||
import org.diylc.core.annotations.ComponentDescriptor;
|
||||
import org.diylc.utils.Pair;
|
||||
|
||||
/**
|
||||
@@ -50,7 +51,40 @@ public class DrawingCache {
|
||||
private static final Logger LOG = Logger.getLogger(DrawingCache.class);
|
||||
|
||||
public static DrawingCache Instance = new DrawingCache();
|
||||
|
||||
/**
|
||||
* Prepares cached renderings of those components from the specified map that have {@link ComponentDescriptor#enableCache()} set to true.
|
||||
*
|
||||
* @param components
|
||||
* @param g2d
|
||||
* @param outlineMode
|
||||
* @param project
|
||||
* @param zoom
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public void bulkPrepare(Map<IDIYComponent<?>, ComponentState> components, G2DWrapper g2d, boolean outlineMode, Project project, double zoom) {
|
||||
// run all renders in parallel to utilize all CPU cores
|
||||
components.entrySet().stream().parallel().forEach(x -> {
|
||||
ComponentType type = ComponentProcessor.getInstance()
|
||||
.extractComponentTypeFrom((Class<? extends IDIYComponent<?>>) x.getKey().getClass());
|
||||
|
||||
if (type.getEnableCache()) {
|
||||
renderAndCache(x.getKey(), g2d, x.getValue(), outlineMode, project, zoom);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a component onto the specified {@link G2DWrapper}. If the component has {@link ComponentDescriptor#enableCache()} set to true,
|
||||
* the drawing will come from the cache. Otherwise, it will be drawn as usual.
|
||||
*
|
||||
* @param component
|
||||
* @param g2d
|
||||
* @param componentState
|
||||
* @param outlineMode
|
||||
* @param project
|
||||
* @param zoom
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public void draw(IDIYComponent<?> component, G2DWrapper g2d, ComponentState componentState,
|
||||
boolean outlineMode, Project project, double zoom) {
|
||||
@@ -60,87 +94,8 @@ public class DrawingCache {
|
||||
if (type.getEnableCache()) {
|
||||
// if we need to apply caching
|
||||
Point firstPoint = component.getControlPoint(0);
|
||||
|
||||
// cache hit!
|
||||
CacheValue value = null;
|
||||
if (imageCache.containsKey(component))
|
||||
value = imageCache.get(component);
|
||||
|
||||
// only honor the cache if the component hasn't changed in the meantime
|
||||
if (value == null || !value.getComponent().equalsTo(component)
|
||||
|| value.getState() != componentState || value.getZoom() != zoom) {
|
||||
// figure out size and placement of buffer image
|
||||
Rectangle2D rect = component.getCachingBounds();
|
||||
int width = (int)Math.round(rect.getWidth() * zoom);
|
||||
int height = (int)Math.round(rect.getHeight() * zoom);
|
||||
// calculate the position of the first point relative to the caching bounds
|
||||
int dx = (int) (firstPoint.x - rect.getX());
|
||||
int dy = (int) (firstPoint.y - rect.getY());
|
||||
|
||||
// create image
|
||||
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||
// create graphics
|
||||
Graphics2D cg2d = image.createGraphics();
|
||||
|
||||
// copy over rendering settings so our cached image has the same features
|
||||
cg2d.setRenderingHints(g2d.getRenderingHints());
|
||||
|
||||
// set clip bounds to the whole image
|
||||
cg2d.setClip(0, 0, width, height);
|
||||
|
||||
// apply zoom
|
||||
if (Math.abs(1.0 - zoom) > 1e-4) {
|
||||
cg2d.scale(zoom, zoom);
|
||||
}
|
||||
// translate to make the top-left corner of the component match (0, 0)
|
||||
cg2d.translate(-firstPoint.x + dx, -firstPoint.y + dy);
|
||||
|
||||
// wrap the graphics so we can track
|
||||
G2DWrapper wrapper = new G2DWrapper(cg2d, zoom);
|
||||
|
||||
// initialize wrapper
|
||||
wrapper.startedDrawingComponent();
|
||||
|
||||
long componentStart = System.nanoTime();
|
||||
|
||||
// now draw the component to the buffer image
|
||||
component.draw(wrapper, componentState, outlineMode, project, wrapper);
|
||||
|
||||
long componentEnd = System.nanoTime();
|
||||
|
||||
Pair<Counter, Counter> stats;
|
||||
String key = component.getClass().getCanonicalName().replace("org.diylc.components.", "");
|
||||
if (renderStatsByType.containsKey(key)) {
|
||||
stats = renderStatsByType.get(key);
|
||||
} else {
|
||||
stats = new Pair<Counter, Counter>(new Counter(), new Counter());
|
||||
renderStatsByType.put(key, stats);
|
||||
}
|
||||
stats.getFirst().add(componentEnd - componentStart);
|
||||
|
||||
// finalize wrapper
|
||||
wrapper.finishedDrawingComponent();
|
||||
|
||||
cg2d.dispose();
|
||||
|
||||
// output cached drawings to separate files
|
||||
// File outputfile = new File("d:\\tmp\\image_" + component.getName() + ".png");
|
||||
// try {
|
||||
// ImageIO.write(image, "png", outputfile);
|
||||
// } catch (IOException e) {
|
||||
// // TODO Auto-generated catch block
|
||||
// e.printStackTrace();
|
||||
// }
|
||||
|
||||
if (wrapper.getCurrentArea() == null || wrapper.getCurrentArea().isEmpty()) {
|
||||
LOG.warn("Empty outline area detected for " + component.getName());
|
||||
}
|
||||
|
||||
// add to the cache
|
||||
value = new CacheValue(component, image, wrapper.getCurrentArea(), wrapper.getContinuityPositiveAreas(),
|
||||
wrapper.getContinuityNegativeAreas(), componentState, zoom, dx, dy);
|
||||
imageCache.put(component, value);
|
||||
}
|
||||
|
||||
CacheValue value = renderAndCache(component, g2d, componentState, outlineMode, project, zoom);
|
||||
|
||||
long componentStart = System.nanoTime();
|
||||
|
||||
@@ -179,7 +134,7 @@ public class DrawingCache {
|
||||
g2d.stopTracking();
|
||||
g2d.stopTrackingContinuityArea();
|
||||
}
|
||||
|
||||
|
||||
public void logStats() {
|
||||
int totalSizeMB = imageCache.values().stream().map(x -> x.image.getData().getDataBuffer().getSize()).reduce(0, Integer::sum) * 4 / 1024 / 1024; // 4 bytes per pixel
|
||||
LOG.debug(String.format("Render cache contains %d elements, approx size is %d MB", imageCache.size(), totalSizeMB));
|
||||
@@ -195,6 +150,95 @@ public class DrawingCache {
|
||||
public void clear() {
|
||||
imageCache.clear();
|
||||
}
|
||||
|
||||
private CacheValue renderAndCache(IDIYComponent<?> component, G2DWrapper g2d, ComponentState componentState,
|
||||
boolean outlineMode, Project project, double zoom) {
|
||||
// if we need to apply caching
|
||||
Point firstPoint = component.getControlPoint(0);
|
||||
|
||||
// cache hit!
|
||||
CacheValue value = null;
|
||||
if (imageCache.containsKey(component))
|
||||
value = imageCache.get(component);
|
||||
|
||||
// only honor the cache if the component hasn't changed in the meantime
|
||||
if (value == null || !value.getComponent().equalsTo(component)
|
||||
|| value.getState() != componentState || value.getZoom() != zoom) {
|
||||
// figure out size and placement of buffer image
|
||||
Rectangle2D rect = component.getCachingBounds();
|
||||
int width = (int)Math.round(rect.getWidth() * zoom);
|
||||
int height = (int)Math.round(rect.getHeight() * zoom);
|
||||
// calculate the position of the first point relative to the caching bounds
|
||||
int dx = (int) (firstPoint.x - rect.getX());
|
||||
int dy = (int) (firstPoint.y - rect.getY());
|
||||
|
||||
// create image
|
||||
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||
// create graphics
|
||||
Graphics2D cg2d = image.createGraphics();
|
||||
|
||||
// copy over rendering settings so our cached image has the same features
|
||||
cg2d.setRenderingHints(g2d.getRenderingHints());
|
||||
|
||||
// set clip bounds to the whole image
|
||||
cg2d.setClip(0, 0, width, height);
|
||||
|
||||
// apply zoom
|
||||
if (Math.abs(1.0 - zoom) > 1e-4) {
|
||||
cg2d.scale(zoom, zoom);
|
||||
}
|
||||
// translate to make the top-left corner of the component match (0, 0)
|
||||
cg2d.translate(-firstPoint.x + dx, -firstPoint.y + dy);
|
||||
|
||||
// wrap the graphics so we can track
|
||||
G2DWrapper wrapper = new G2DWrapper(cg2d, zoom);
|
||||
|
||||
// initialize wrapper
|
||||
wrapper.startedDrawingComponent();
|
||||
|
||||
long componentStart = System.nanoTime();
|
||||
|
||||
// now draw the component to the buffer image
|
||||
component.draw(wrapper, componentState, outlineMode, project, wrapper);
|
||||
|
||||
long componentEnd = System.nanoTime();
|
||||
|
||||
Pair<Counter, Counter> stats;
|
||||
String key = component.getClass().getCanonicalName().replace("org.diylc.components.", "");
|
||||
if (renderStatsByType.containsKey(key)) {
|
||||
stats = renderStatsByType.get(key);
|
||||
} else {
|
||||
stats = new Pair<Counter, Counter>(new Counter(), new Counter());
|
||||
renderStatsByType.put(key, stats);
|
||||
}
|
||||
stats.getFirst().add(componentEnd - componentStart);
|
||||
|
||||
// finalize wrapper
|
||||
wrapper.finishedDrawingComponent();
|
||||
|
||||
cg2d.dispose();
|
||||
|
||||
// output cached drawings to separate files
|
||||
// File outputfile = new File("d:\\tmp\\image_" + component.getName() + ".png");
|
||||
// try {
|
||||
// ImageIO.write(image, "png", outputfile);
|
||||
// } catch (IOException e) {
|
||||
// // TODO Auto-generated catch block
|
||||
// e.printStackTrace();
|
||||
// }
|
||||
|
||||
if (wrapper.getCurrentArea() == null || wrapper.getCurrentArea().isEmpty()) {
|
||||
LOG.warn("Empty outline area detected for " + component.getName());
|
||||
}
|
||||
|
||||
// add to the cache
|
||||
value = new CacheValue(component, image, wrapper.getCurrentArea(), wrapper.getContinuityPositiveAreas(),
|
||||
wrapper.getContinuityNegativeAreas(), componentState, zoom, dx, dy);
|
||||
imageCache.put(component, value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private class CacheValue {
|
||||
IDIYComponent<?> component;
|
||||
|
||||
@@ -254,13 +254,10 @@ public class DrawingManager {
|
||||
// AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f);
|
||||
|
||||
// g2dWrapper.resetTx();
|
||||
|
||||
// componentAreaMap.clear();
|
||||
|
||||
// put components and their states in a map
|
||||
Map<IDIYComponent<?>, ComponentState> componentStateMap = new HashMap<IDIYComponent<?>, ComponentState>();
|
||||
for (IDIYComponent<?> component : project.getComponents()) {
|
||||
// Do not draw the component if it's filtered out.
|
||||
if (filter != null && !filter.testComponent(component)) {
|
||||
continue;
|
||||
}
|
||||
ComponentState state = ComponentState.NORMAL;
|
||||
if (drawOptions.contains(DrawOption.SELECTION) && selectedComponents.contains(component)) {
|
||||
if (dragInProgress) {
|
||||
@@ -269,6 +266,23 @@ public class DrawingManager {
|
||||
state = ComponentState.SELECTED;
|
||||
}
|
||||
}
|
||||
componentStateMap.put(component, state);
|
||||
}
|
||||
|
||||
boolean outlineMode = drawOptions.contains(DrawOption.OUTLINE_MODE);
|
||||
|
||||
// give the cache a chance to prepare all the components in multiple threads
|
||||
if (drawOptions.contains(DrawOption.ENABLE_CACHING)) {
|
||||
DrawingCache.Instance.bulkPrepare(componentStateMap, g2dWrapper, outlineMode, project, zoom);
|
||||
}
|
||||
|
||||
// componentAreaMap.clear();
|
||||
for (IDIYComponent<?> component : project.getComponents()) {
|
||||
// Do not draw the component if it's filtered out.
|
||||
if (filter != null && !filter.testComponent(component)) {
|
||||
continue;
|
||||
}
|
||||
ComponentState state = componentStateMap.get(component);
|
||||
// Do not track the area if component is not invalidated and was
|
||||
// drawn in the same state.
|
||||
boolean trackArea = lastDrawnStateMap.get(component) != state;
|
||||
@@ -289,7 +303,7 @@ public class DrawingManager {
|
||||
if (drawOptions.contains(DrawOption.ENABLE_CACHING)) // go through the DrawingCache
|
||||
DrawingCache.Instance.draw(component, g2dWrapper, state, drawOptions.contains(DrawOption.OUTLINE_MODE), project, zoom);
|
||||
else // go stragiht to the wrapper
|
||||
component.draw(g2dWrapper, state, drawOptions.contains(DrawOption.OUTLINE_MODE), project, g2dWrapper);
|
||||
component.draw(g2dWrapper, state, outlineMode, project, g2dWrapper);
|
||||
|
||||
if (g2dWrapper.isTrackingContinuityArea()) {
|
||||
LOG.info("Component " + component.getName() + " of type " + component.getClass().getName() + " did not stop tracking continuity area.");
|
||||
@@ -652,32 +666,53 @@ public class DrawingManager {
|
||||
List<Area> newAreas = new ArrayList<Area>();
|
||||
boolean[] consumed = new boolean[areas.size()];
|
||||
for (int i = 0; i < areas.size(); i++) {
|
||||
Area a1 = areas.get(i);
|
||||
Rectangle2D bounds1 = a1.getBounds2D();
|
||||
|
||||
Map<Area, Integer> candidates = new HashMap<Area, Integer>();
|
||||
|
||||
for (int j = i + 1; j < areas.size(); j++) {
|
||||
if (consumed[j])
|
||||
continue;
|
||||
Area a1 = areas.get(i);
|
||||
// if (consumed[j])
|
||||
// continue;
|
||||
Area a2 = areas.get(j);
|
||||
Area intersection = null;
|
||||
if (a1.getBounds2D().intersects(a2.getBounds())) {
|
||||
intersection = new Area(a1);
|
||||
intersection.intersect(a2);
|
||||
}
|
||||
// if the two areas intersect, make a union and consume the second area
|
||||
if (intersection != null && !intersection.isEmpty()) {
|
||||
a1.add(a2);
|
||||
consumed[j] = true;
|
||||
} else { // maybe there's a connection between them
|
||||
for (Connection p : connections) { // use getBounds to optimize the computation, don't get into complex math if not needed
|
||||
if ((a1.getBounds().contains(p.getP1()) && a2.getBounds().contains(p.getP2()) && a1.contains(p.getP1()) && a2.contains(p.getP2())) ||
|
||||
(a1.getBounds().contains(p.getP2()) && a2.getBounds().contains(p.getP1())) && a1.contains(p.getP2()) && a2.contains(p.getP1())) {
|
||||
a1.add(a2);
|
||||
consumed[j] = true;
|
||||
break;
|
||||
}
|
||||
Rectangle2D bounds2 = a2.getBounds2D();
|
||||
|
||||
// maybe there's a direct connection between them, try that first because it's faster than area overlap
|
||||
for (Connection p : connections) { // use getBounds to optimize the computation, don't get into complex math if not needed
|
||||
if ((bounds1.contains(p.getP1()) && bounds2.contains(p.getP2()) && a1.contains(p.getP1()) && a2.contains(p.getP2())) ||
|
||||
(bounds1.contains(p.getP2()) && bounds2.contains(p.getP1())) && a1.contains(p.getP2()) && a2.contains(p.getP1())) {
|
||||
a1.add(a2);
|
||||
consumed[j] = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (bounds1.intersects(bounds2)) {
|
||||
candidates.put(a2, j);
|
||||
}
|
||||
}
|
||||
|
||||
Set<Area> mergeQueue = new HashSet<Area>();
|
||||
|
||||
// run a parallel for-each to determine which areas overlap with a1 and add them to a queue to be added to a1 later
|
||||
if (candidates.size() > 0) {
|
||||
candidates.entrySet().parallelStream().forEach(x -> {
|
||||
Area intersection = new Area(a1);
|
||||
intersection.intersect(x.getKey());
|
||||
if (intersection != null && !intersection.isEmpty()) {
|
||||
synchronized (mergeQueue) {
|
||||
mergeQueue.add(x.getKey());
|
||||
}
|
||||
consumed[x.getValue()] = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// if make a union of all intersecting areas
|
||||
for(Area a2 : mergeQueue)
|
||||
a1.add(a2);
|
||||
}
|
||||
|
||||
for (int i = 0; i < areas.size(); i++)
|
||||
if (!consumed[i])
|
||||
newAreas.add(areas.get(i));
|
||||
|
||||
Reference in New Issue
Block a user