Skip to content

Commit b52aad5

Browse files
Optimize ImageBuffer, split map selection painting out so buffered maps aren't redrawn every selection change, be more selective when to y-flip data
1 parent cd15637 commit b52aad5

12 files changed

Lines changed: 228 additions & 98 deletions

File tree

Framework/Cyclops/Cyclops/src/main/java/org/peakaboo/framework/cyclops/visualization/drawing/map/MapDrawing.java

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.peakaboo.framework.cyclops.visualization.drawing.Drawing;
1313
import org.peakaboo.framework.cyclops.visualization.drawing.DrawingRequest;
1414
import org.peakaboo.framework.cyclops.visualization.drawing.map.painters.MapPainter;
15+
import org.peakaboo.framework.cyclops.visualization.drawing.map.painters.SelectionMaskPainter;
1516
import org.peakaboo.framework.cyclops.visualization.drawing.painters.PainterData;
1617
import org.peakaboo.framework.cyclops.visualization.drawing.painters.axis.AxisPainter;
1718
import org.peakaboo.framework.cyclops.visualization.palette.Palette;
@@ -126,8 +127,13 @@ public void setPainters(MapPainter painter) {
126127

127128

128129
/**
129-
* Draws a map using the given data and the given set of {@link MapPainter}s
130-
*
130+
* Draws a map using the given data and the given set of {@link MapPainter}s.
131+
* <p>
132+
* Note: {@link SelectionMaskPainter}s are <em>deferred</em> and are not drawn here.
133+
* The selection is rendered separately via
134+
* {@link #drawSelectionOverlay(MapPainter)} so that callers can cache the base map
135+
* and recomposite a cheap selection overlay on top without re-rendering everything.
136+
*
131137
* @param data
132138
* the data to use to draw the map
133139
* @param mapPainters
@@ -136,9 +142,9 @@ public void setPainters(MapPainter painter) {
136142
@Override
137143
public void draw()
138144
{
139-
145+
140146
if (context == null) return;
141-
147+
142148
float oldMaxIntensity = dr.maxYIntensity;
143149

144150
Coord<Bounds<Float>> borderSizes = calcAxisBorders();
@@ -149,13 +155,15 @@ public void draw()
149155

150156

151157
context.translate(borderSizes.x.start, borderSizes.y.start);
152-
158+
153159
for (MapPainter t : painters) {
160+
// Selection masks are drawn separately as an overlay (see class note above)
161+
if (t instanceof SelectionMaskPainter) continue;
154162
t.draw(new PainterData(context, dr, mapDimensions, null));
155163
}
156164

157165

158-
166+
159167
context.restore();
160168

161169

@@ -208,6 +216,27 @@ public void draw()
208216
}
209217

210218

219+
/**
220+
* Draws a single selection-overlay painter on top of an already-drawn base map,
221+
* using the same border translate and cell geometry as {@link #draw()}. This lets
222+
* a selection change be recomposited cheaply without re-rendering the base map or
223+
* its axes.
224+
*
225+
* @param selectionPainter the selection painter to draw; ignored if {@code null}
226+
*/
227+
public void drawSelectionOverlay(MapPainter selectionPainter)
228+
{
229+
if (context == null || selectionPainter == null) return;
230+
231+
Coord<Bounds<Float>> borderSizes = calcAxisBorders();
232+
Coord<Float> mapDimensions = calcMapSize();
233+
234+
context.save();
235+
context.translate(borderSizes.x.start, borderSizes.y.start);
236+
selectionPainter.draw(new PainterData(context, dr, mapDimensions, null));
237+
context.restore();
238+
}
239+
211240

212241
/**
213242
* Calculates the dimensions of the map. Aspect ratio is preserved, and image dimensions are drawn from

Framework/Cyclops/Cyclops/src/main/java/org/peakaboo/framework/cyclops/visualization/drawing/map/painters/RasterColorMapPainter.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,21 +60,26 @@ public synchronized void drawMap(PainterData p, float cellSize, float rawCellSiz
6060

6161
p.context.save();
6262

63-
IntArrayList data = transformListDataForMap(p.dr, pixels);
64-
6563
if (p.dr.drawToVectorSurface) {
64+
// Vector surfaces can't be buffered, so we always re-transform and draw.
65+
IntArrayList data = transformListDataForMap(p.dr, pixels);
6666
drawAsScalar(p, data, cellSize);
6767
buffer = null;
6868
} else {
69-
69+
7070
if (buffer == null || buffer.getWidth() != p.dr.dataWidth || buffer.getHeight() != p.dr.dataHeight) {
7171
buffer = createRasterBuffer(p);
72+
// A freshly allocated buffer is blank and must be filled.
73+
stale = true;
7274
}
7375
if (stale) {
76+
// Only pay for the flip/transform when we actually need to rewrite
77+
// the buffer; otherwise we just recomposite the cached pixels.
78+
IntArrayList data = transformListDataForMap(p.dr, pixels);
7479
drawToRasterBuffer(data, p.dr.dataHeight * p.dr.dataWidth);
7580
}
7681
p.context.compose(buffer, 0, 0, cellSize);
77-
82+
7883
}
7984

8085
p.context.restore();
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.peakaboo.framework.cyclops.visualization.drawing.map.painters;
22

3+
import java.util.Arrays;
34
import java.util.logging.Level;
45

56
import org.peakaboo.framework.cyclops.log.CyclopsLog;
@@ -9,6 +10,8 @@
910

1011
public class SelectionMaskPainter extends RasterColorMapPainter {
1112

13+
private static final int TRANSPARENT_ARGB = 0;
14+
1215
private int sizeX, sizeY;
1316
private int selectionColour;
1417
private IntArrayList colours;
@@ -21,29 +24,27 @@ public SelectionMaskPainter(PaletteColour c, IntArrayList points, int sizeX, int
2124
}
2225

2326
public synchronized void configure(int dataWidth, int dataHeight, IntArrayList points) {
24-
int size = sizeX*sizeY;
25-
2627
if (this.sizeX != dataWidth || this.sizeY != dataHeight) {
2728
super.buffer = null;
2829
this.sizeX = dataWidth;
2930
this.sizeY = dataHeight;
3031
}
3132

32-
colours.clear();
33-
33+
// Size is derived from the incoming dimensions, not the (possibly stale) fields.
34+
int size = sizeX * sizeY;
35+
3436
// don't bother updating the pixel list, we won't be drawing anything when no
3537
// points are in the selection.
3638
if (points.isEmpty()) {
3739
setEnabled(false);
3840
return;
3941
}
40-
41-
PaletteColour transparent = new PaletteColour(0, 0, 0, 0);
42-
int transparentARGB = transparent.getARGB();
43-
for (int i = 0; i < size; i++) {
44-
colours.add(transparentARGB);
45-
}
46-
42+
43+
// Reuse the backing array rather than reallocating: ensure it's the right
44+
// length, reset every pixel to transparent (ARGB 0), then mark selected points.
45+
colours.size(size);
46+
Arrays.fill(colours.elements(), 0, size, TRANSPARENT_ARGB);
47+
4748
for (int i = 0; i < points.size(); i++) {
4849
int point = points.getInt(i);
4950
if (point >= size || point < 0) {
@@ -55,7 +56,7 @@ public synchronized void configure(int dataWidth, int dataHeight, IntArrayList p
5556

5657
setPixels(colours);
5758
setEnabled(true);
58-
59+
5960
}
6061

6162
}

Framework/Cyclops/CyclopsSwing/src/main/java/org/peakaboo/framework/cyclops/visualization/backend/awt/surfaces/graphics/ImageBuffer.java

Lines changed: 57 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,91 @@
11
package org.peakaboo.framework.cyclops.visualization.backend.awt.surfaces.graphics;
22

33

4+
import java.awt.AlphaComposite;
45
import java.awt.Graphics2D;
56
import java.awt.image.BufferedImage;
7+
import java.awt.image.DataBufferInt;
68
import java.util.ArrayList;
79
import java.util.Arrays;
810
import java.util.List;
911

1012
import org.peakaboo.framework.cyclops.visualization.Buffer;
1113
import org.peakaboo.framework.cyclops.visualization.palette.PaletteColour;
12-
import org.peakaboo.framework.stratus.api.Stratus;
1314

15+
/**
16+
* A {@link Buffer} backed by a {@link BufferedImage}.
17+
* <p>
18+
* The backing image is always a {@link BufferedImage#TYPE_INT_ARGB} image, so pixel
19+
* access reads and writes the image's live {@code int[]} raster directly. This avoids
20+
* the full-image {@code getRGB}/{@code setRGB} copies the old implementation paid on
21+
* every buffer build, which matters a great deal for large maps.
22+
* <p>
23+
* {@code TYPE_INT_ARGB} is <em>non-premultiplied</em>, so the stored {@code int}
24+
* values are exactly the straight ARGB values passed to {@link #setPixelARGB}, which
25+
* keeps semi-transparent pixels (e.g. selection masks) correct.
26+
* <p>
27+
* Grabbing the live raster array disables Java2D's managed-image acceleration for that
28+
* image, which is fine for buffers we fill pixel-by-pixel on the CPU. Buffers that are
29+
* only ever drawn to via {@link Graphics2D} (e.g. a large compositing buffer) never
30+
* touch the pixel array — {@link #clear()} uses {@link AlphaComposite#Clear} in that
31+
* case — so they remain managed and accelerated.
32+
*/
1433
public class ImageBuffer extends ScreenSurface implements Buffer
1534
{
1635

17-
private BufferedImage image;
18-
19-
private boolean dirty = false;
20-
private int[] datasource;
36+
private final BufferedImage image;
37+
38+
/**
39+
* The image's live raster array, lazily fetched on first pixel access. Null until
40+
* then, so Graphics2D-only buffers never grab it (and stay managed/accelerated).
41+
*/
42+
private int[] datasource;
43+
2144

22-
2345
public ImageBuffer(int x, int y) {
24-
this(Stratus.acceleratedImage(x, y));
46+
this(new BufferedImage(x, y, BufferedImage.TYPE_INT_ARGB));
2547
}
2648

27-
public ImageBuffer(BufferedImage image) {
49+
private ImageBuffer(BufferedImage image) {
2850
super((Graphics2D) image.getGraphics());
2951
this.image = image;
3052
}
31-
private synchronized void init()
32-
{
53+
54+
/**
55+
* Lazily obtains the image's live raster array. Writes through it are immediately
56+
* visible on the image.
57+
*/
58+
private int[] pixels() {
3359
if (datasource == null) {
34-
datasource = image.getRGB(0, 0, image.getWidth(), image.getHeight(), null, 0, image.getWidth());
60+
datasource = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
3561
}
62+
return datasource;
3663
}
3764

3865
@Override
3966
public BufferedImage getImageSource()
4067
{
41-
commitChanges();
68+
// The live raster is the source of truth - nothing to flush.
4269
return image;
4370
}
4471

45-
private void commitChanges()
46-
{
47-
if (dirty) {
48-
image.setRGB(0, 0, image.getWidth(), image.getHeight(), datasource, 0, image.getWidth());
49-
}
50-
dirty = false;
51-
}
52-
5372
@Override
5473
public void clear() {
55-
if (datasource == null) {
56-
init();
74+
if (datasource != null) {
75+
// We've already taken the pixel array; clear it in place.
76+
Arrays.fill(datasource, 0);
77+
} else {
78+
// Clear via the graphics context so the image stays managed/accelerated.
79+
// This is the path large compositing buffers take, since they are never
80+
// pixel-poked and so never grab the raster array.
81+
Graphics2D g = (Graphics2D) image.getGraphics();
82+
try {
83+
g.setComposite(AlphaComposite.Clear);
84+
g.fillRect(0, 0, image.getWidth(), image.getHeight());
85+
} finally {
86+
g.dispose();
87+
}
5788
}
58-
Arrays.fill(datasource, 0);
59-
commitChanges();
6089
}
6190

6291
@Override
@@ -68,18 +97,12 @@ public void setPixelValue(int x, int y, PaletteColour c) {
6897
public void setPixelValue(int offset, PaletteColour c) {
6998
setPixelARGB(offset, c.getARGB());
7099
}
71-
100+
72101
@Override
73102
public void setPixelARGB(int offset, int c) {
74-
if (datasource == null) {
75-
init();
76-
}
77-
dirty = true;
78-
datasource[offset] = c;
103+
pixels()[offset] = c;
79104
}
80105

81-
82-
83106

84107
@Override
85108
public PaletteColour getPixelValue(int x, int y) {
@@ -89,12 +112,7 @@ public PaletteColour getPixelValue(int x, int y) {
89112

90113
@Override
91114
public PaletteColour getPixelValue(int index) {
92-
93-
if (datasource == null) {
94-
init();
95-
}
96-
97-
return new PaletteColour(datasource[index]);
115+
return new PaletteColour(pixels()[index]);
98116
}
99117

100118
@Override
@@ -127,7 +145,7 @@ public List<Integer> getPixelsARGB() {
127145

128146
@Override
129147
public int getPixelARGB(int index) {
130-
return datasource[index];
148+
return pixels()[index];
131149
}
132150

133151
@Override

Framework/Stratus/src/main/java/org/peakaboo/framework/stratus/components/ui/layers/ModalLayer.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import java.awt.Component;
77
import java.awt.Graphics;
88
import java.awt.Graphics2D;
9+
import java.awt.Rectangle;
910
import java.awt.TexturePaint;
1011
import java.awt.event.InputEvent;
1112
import java.awt.event.KeyEvent;
@@ -125,12 +126,22 @@ public void paint(Graphics g) {
125126
if (Stratus.lowGraphicsMode) {
126127
super.paint(g);
127128
} else {
128-
//Paint into a buffer
129+
//Paint into a buffer, limiting the work to the region being repainted
130+
//on screen. The texture fill below is clipped to that same region, so
131+
//the rest of the buffer is never read from on this paint.
129132
buffered.resize(this.getWidth(), this.getHeight());
130-
buffered.clear();
131133
var buffer = buffered.get();
132134
var bufg = buffer.createGraphics();
133-
135+
if (g.getClip() != null) {
136+
bufg.setClip(g.getClip());
137+
}
138+
Rectangle dirty = bufg.getClipBounds();
139+
if (dirty == null) {
140+
dirty = new Rectangle(0, 0, this.getWidth(), this.getHeight());
141+
}
142+
bufg.setBackground(new Color(255, 255, 255, 0));
143+
bufg.clearRect(dirty.x, dirty.y, dirty.width, dirty.height);
144+
134145
Graphics2D b2d = Stratus.modernGraphicsSettings(bufg);
135146
super.paint(b2d);
136147
buffered.markPainted();

0 commit comments

Comments
 (0)