Analysis
Overview
The goal of this task is to search for and implement some optimizations connected with the drawing of scenes. SimpleSceneDemoTest will be used to measure how effective these optimisations are.
Task requirements
- Now SimpleSceneDemoTest redraws the scene in about 1220 milliseconds.
- Improve this time by at least 20 percent.
Task result
The result of this task is code.
Implementation idea
- When drawing a scene element, we take into account the colours of all its ancestors. In the case of an image, this mean multiplying every pixel by the effective colour, which is a heavy operation. Since the effective colour changes much more rarely than the scene is redrawn, it is a good idea to cache the image multiplied by recently used effective colours.
- If the user imports the same image in two or more frames, there is no need to load it twice. It might be a good idea to cache the recently imported images, too.
- Try changing the preferred image type from BufferedImage.TYPE_INT_ARGB to BufferedImage.TYPE_INT_ARGB_PRE (the second format stores the values for red, green and blue divided by alpha, while the first one computes them on every step)
Related
How to demo
Compare the performance of SimpleSceneDemoTest before and after this task.
Design
The first two implementation ideas will be implemented, the third one did not prove effective and will not.
ImmImage should be made really immutable and should not expose representation, so that we can cache some things in it.
The constructor ImmImage(BufferedImage image) should make a deep copy of the argument. Thus if the caller keeps a reference to the buffered image and makes some changes to it, they will not affect the ImmImage.
The same applies for the method BufferedImage toBufferedImage() and its result.
The other constructors will also make a copy of the image in order to be sure that the image is in the preferred format.
A new private method will be created and the constructors and toBufferedImage() will use it:
/** * Deep copy of BufferedImage * * @param source * the source image * @return * a copy of the source image converted to out preferred type */ private static BufferedImage copyImage(BufferedImage source) { BufferedImage newImage = new BufferedImage(source.getWidth(), source .getHeight(), ImmImage.PREFERRED_BI_TYPE); Graphics2D gt = newImage.createGraphics(); gt.drawImage(source, 0, 0, null); gt.dispose(); return newImage; }
Some helper methods will be added to minimize the need to convert to BufferedImage:
- a method drawing the image into graphics.
The ImageElementHelper will not draw the image itself, but will call this method. It is necessary to do so, because ImmImage has direct access to the wrapped BufferedImage, while the element helper would need to copy it.
/** * Draws the image. * @param g2d * The graphics where the image will be drawn. * */ public void paint(Graphics2D g2d) { // implementation }
- a method multiplying the image by a given color
It already exists in ElementHelper, but will be moved here for the same reasons as paint(Graphics)
/** * Multiplies all image pixels by given color. * * @param color * The color by which to multiply. * @return * The image multiplied by the given color. */ public ImmImage transformColor(ImmColor color) { // implementation }
Caching ImmImages multiplied by recently used colours will make drawing of images above other elements (including background) faster
- A new private field will be added in ImmImage to store the image multiplied by recently used colours - HashMap<ImmColor, ImmImage> colorImgCache.
- The method transformColors(ImmColor) will check if the image multiplied by the given color is already cached. If it is it will return the cached value. If not - it will compute it and put it in the cache.
Caching recently loaded ImmImages will prevent us from loading the same image more than once.
- All constructors except the one with BufferedImage will be deleted or made private.
- A new method will be created instead of them:
/** * Loads an ImmImage from the given URL. * @param url * The URL from which to load the image. * @return * The loaded image */ public static ImmImage loadCached(URL url) { if (!loadedImages.containsKey(url)) { try { InputStream is = null; try { is = url.openStream(); loadedImages.put(url, new ImmImage(is)); } finally { if (is != null) { is.close(); } } } catch (IOException e) { throw new RuntimeException(e); } } return loadedImages.get(url); }
It is a good idea to create a new class representing a map which keeps weak/soft references to its values. Otherwise we could run out of memory.
However most probably this won't be done now.
Implementation
Done according to the design: [3553], [3645], [3646]