Skip to content

Commit 841bae1

Browse files
author
Nicolai Parlog
committed
Demonstrate vector API
1 parent 4072bbb commit 841bae1

File tree

7 files changed

+309
-0
lines changed

7 files changed

+309
-0
lines changed

Diff for: README.md

+5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ These are the categories:
1111
* [Runtime](#runtime)
1212
* [Internals](#internals)
1313

14+
Circled numbers like ⑰ indicate which Java version introduced a feature or a change.
15+
A circled X ⓧ indicates an incubating or previewing feature that is not yet standardized - it can be experimented with but is subject to change.
16+
1417
For a more practical approach to many of these features, including some quick code metrics, check my [_Java After Eight_ repo](https://github.com/nipafx/java-after-eight).
1518
You can find more from me on [nipafx.dev](https://nipafx.dev) as well as on [YouTube](https://youtube.com/nipafx) and [Twitch](https://twitch.tv/nipafx).
1619
To get in touch, follow me [on Twitter](https://twitter.com/nipafx) (DMs are open) or join [my Discord](https://discord.com/invite/7m9w8Td).
@@ -90,6 +93,8 @@ Check out the [j_ms](https://nipafx.dev/#tags~~j_ms) tag on my blog (for example
9093

9194
If an API that was introduced in Java 9+ was later updated, the update is listed in the next section.
9295

96+
*[vector API](src/main/java/dev/nipafx/demo/java_next/api/vector)
97+
([JEP 426](https://openjdk.java.net/jeps/426))
9398
*[address resolution SPI](src/main/java/dev/nipafx/demo/java18/api/ip_resolution)
9499
([JEP 418](http://openjdk.java.net/jeps/418))
95100
*[random generator](src/main/java/dev/nipafx/demo/java17/api/random)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
package dev.nipafx.demo.java_next.api.vector;
2+
3+
import jdk.incubator.vector.ByteVector;
4+
import jdk.incubator.vector.VectorMask;
5+
import jdk.incubator.vector.VectorOperators;
6+
import jdk.incubator.vector.VectorShuffle;
7+
import jdk.incubator.vector.VectorSpecies;
8+
9+
import javax.imageio.ImageIO;
10+
import java.awt.Point;
11+
import java.awt.image.BufferedImage;
12+
import java.awt.image.DataBufferByte;
13+
import java.awt.image.Raster;
14+
import java.io.IOException;
15+
import java.nio.file.Path;
16+
import java.util.function.UnaryOperator;
17+
18+
/**
19+
* After running this demo, compare the city images in `src/main/resources` with those
20+
* created in `target`.
21+
*/
22+
public class ImageColors {
23+
24+
private static final VectorSpecies<Byte> RGB_SPECIES = ByteVector.SPECIES_PREFERRED;
25+
private static final int RGB_STEPS = RGB_SPECIES.length() - RGB_SPECIES.length() % 3;
26+
27+
private static final VectorShuffle<Byte> COLOR_SHUFFLE = VectorShuffle.fromOp(RGB_SPECIES, ImageColors::rotateRgbValues);
28+
private static final VectorMask<Byte> PURPLE_SHIFT = VectorShuffle
29+
// Colors appear in image byte array in order BLUE, GREEN, RED. Only
30+
// BLUE and RED with indices 0, 3, 6, ... and 2, 5, 8, ... respectively
31+
// should be set, so indices 1, 4, 7, etc... should be unset in the mask.
32+
.fromOp(RGB_SPECIES, index -> index % 3 == 1 ? -1 : 1)
33+
.laneIsValid();
34+
35+
public static void main(String[] args) throws IOException {
36+
logVectorInformation();
37+
convertAll(new Conversion("Inverting", "inverted-%s", ImageColors::invertColors));
38+
convertAll(new Conversion("Inverting (with vectors)", "inverted-%s-vector", ImageColors::invertColors_vectorized));
39+
convertAll(new Conversion("Rotating", "rotated-%s", ImageColors::rotateColors));
40+
convertAll(new Conversion("Rotating (with vectors)", "rotated-%s-vector", ImageColors::rotateColors_vectorized));
41+
convertAll(new Conversion("Purple-shifting", "shifted-%s", ImageColors::purpleShift));
42+
convertAll(new Conversion("Purple-shifting (with vectors)", "shifted-%s-vector", ImageColors::purpleShift_vectorized));
43+
}
44+
45+
private static void logVectorInformation() {
46+
System.out.println("Number of lanes: " + RGB_SPECIES.length());
47+
System.out.println("Number of RGB values per loop: " + RGB_STEPS);
48+
System.out.println();
49+
}
50+
51+
private static void convertAll(Conversion conversion) throws IOException {
52+
convert("hcmc", conversion);
53+
convert("hk", conversion);
54+
convert("tokyo", conversion);
55+
System.out.println();
56+
}
57+
58+
private static void convert(String imageName, Conversion conversion) throws IOException {
59+
var imageUrl = ImageColors.class.getClassLoader().getResource(imageName + ".jpg");
60+
var image = ImageIO.read(imageUrl);
61+
byte[] imageData = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
62+
63+
System.out.printf("%s %s ...", conversion.logMessageVerb(), imageName);
64+
long startTime = System.currentTimeMillis();
65+
var newImageData = conversion.converter().apply(imageData);
66+
long runTime = System.currentTimeMillis() - startTime;
67+
System.out.printf(" %.3fms%n", (runTime / 1_000d));
68+
69+
var newImageBuffer = new DataBufferByte(newImageData, newImageData.length);
70+
var newImageRaster = Raster.createRaster(image.getSampleModel(), newImageBuffer, new Point(0, 0));
71+
var newImage = new BufferedImage(image.getWidth(), image.getHeight(), image.getType());
72+
newImage.setData(newImageRaster);
73+
74+
var newImageName = conversion.fileNameFormat().formatted(imageName) + ".jpg";
75+
var newImageFile = Path.of("target", newImageName).toAbsolutePath().toFile();
76+
ImageIO.write(newImage, "JPG", newImageFile);
77+
}
78+
79+
interface ImageConverter extends UnaryOperator<byte[]> { }
80+
81+
record Conversion(String logMessageVerb, String fileNameFormat, ImageConverter converter) { }
82+
83+
/*
84+
* INVERTING COLORS
85+
*/
86+
87+
private static byte[] invertColors(byte[] image) {
88+
byte[] newImage = new byte[image.length];
89+
90+
for (int pixel = 0; pixel + 2 < image.length; pixel += 3) {
91+
byte blue = image[pixel];
92+
byte green = image[pixel + 1];
93+
byte red = image[pixel + 2];
94+
95+
byte newRed = (byte) -red;
96+
byte newGreen = (byte) -green;
97+
byte newBlue = (byte) -blue;
98+
99+
newImage[pixel] = newBlue;
100+
newImage[pixel + 1] = newGreen;
101+
newImage[pixel + 2] = newRed;
102+
}
103+
104+
return newImage;
105+
}
106+
107+
private static byte[] invertColors_vectorized(byte[] image) {
108+
byte[] newImage = new byte[image.length];
109+
110+
// The species can be larger than the steps taken in each loop
111+
// (i.e. `RGB_SPECIES.length() >= RGB_STEPS`), which creates
112+
// the risk that the last iteration wants to write to an array
113+
// that could contain `RGB_STEPS` more values but not
114+
// `RGB_SPECIES.length()` more values. To prevent that, execute
115+
// one fewer vectorized loop.
116+
int loopBound = RGB_SPECIES.loopBound(image.length) - RGB_STEPS;
117+
int pixel = 0;
118+
// vectorized loop
119+
for (; pixel < loopBound; pixel += RGB_STEPS) {
120+
var rgbValues = ByteVector.fromArray(RGB_SPECIES, image, pixel);
121+
var newRgbValues = rgbValues.neg();
122+
newRgbValues.intoArray(newImage, pixel);
123+
}
124+
// remainder
125+
for (; pixel + 2 < image.length; pixel += 3) {
126+
byte blue = image[pixel];
127+
byte green = image[pixel + 1];
128+
byte red = image[pixel + 2];
129+
130+
byte newRed = (byte) -red;
131+
byte newGreen = (byte) -green;
132+
byte newBlue = (byte) -blue;
133+
134+
newImage[pixel] = newBlue;
135+
newImage[pixel + 1] = newGreen;
136+
newImage[pixel + 2] = newRed;
137+
}
138+
139+
return newImage;
140+
}
141+
142+
/*
143+
* ROTATING COLORS
144+
*/
145+
146+
private static byte[] rotateColors(byte[] image) {
147+
byte[] newImage = new byte[image.length];
148+
149+
for (int pixel = 0; pixel + 2 < image.length; pixel += 3) {
150+
byte blue = image[pixel];
151+
byte green = image[pixel + 1];
152+
byte red = image[pixel + 2];
153+
154+
newImage[pixel] = red;
155+
newImage[pixel + 1] = blue;
156+
newImage[pixel + 2] = green;
157+
}
158+
159+
return newImage;
160+
}
161+
162+
private static byte[] rotateColors_vectorized(byte[] image) {
163+
byte[] newImage = new byte[image.length];
164+
165+
// see comment in `invertColors_vectorized`
166+
int loopBound = RGB_SPECIES.loopBound(image.length) - RGB_STEPS;
167+
int pixel = 0;
168+
// vectorized loop
169+
for (; pixel < loopBound; pixel += RGB_STEPS) {
170+
var rgbValues = ByteVector.fromArray(RGB_SPECIES, image, pixel);
171+
var newRgbValues = rgbValues.rearrange(COLOR_SHUFFLE);
172+
newRgbValues.intoArray(newImage, pixel);
173+
}
174+
// remainder
175+
for (; pixel + 2 < image.length; pixel += 3) {
176+
byte blue = image[pixel];
177+
byte green = image[pixel + 1];
178+
byte red = image[pixel + 2];
179+
180+
newImage[pixel] = red;
181+
newImage[pixel + 1] = blue;
182+
newImage[pixel + 2] = green;
183+
}
184+
185+
return newImage;
186+
}
187+
188+
/**
189+
* Rotate RGB values within each triple, but not across triples
190+
* @param newIndex the index in the shuffled vector
191+
* @return the index in the old vector mapped to the new one
192+
*/
193+
private static int rotateRgbValues(int newIndex) {
194+
if (newIndex >= RGB_STEPS)
195+
return newIndex;
196+
197+
int newValueIndexInTriple = newIndex % 3;
198+
int tripleStartIndex = newIndex - newValueIndexInTriple;
199+
200+
int oldValueIndexInTriple = Math.floorMod(newValueIndexInTriple - 1, 3);
201+
return tripleStartIndex + oldValueIndexInTriple;
202+
}
203+
204+
/*
205+
* SHIFTING COLORS
206+
*/
207+
208+
private static byte[] purpleShift(byte[] image) {
209+
byte[] newImage = new byte[image.length];
210+
211+
double imageLength = image.length;
212+
for (int pixel = 0; pixel + 2 < image.length; pixel += 3) {
213+
double purpleQuotient = (pixel / imageLength);
214+
// ignores one-complement and maps [0d...127d; 128d...255d] to [0...127, -128...-1]
215+
byte purpleIndex = (byte) (255 * purpleQuotient);
216+
217+
byte blue = image[pixel];
218+
byte green = image[pixel + 1];
219+
byte red = image[pixel + 2];
220+
221+
// boost blue and red to tint purple
222+
byte newBlue = maxByte(purpleIndex, blue);
223+
byte newRed = maxByte(purpleIndex, red);
224+
225+
newImage[pixel] = newBlue;
226+
newImage[pixel + 1] = green;
227+
newImage[pixel + 2] = newRed;
228+
}
229+
230+
return newImage;
231+
}
232+
233+
private static byte[] purpleShift_vectorized(byte[] image) {
234+
byte[] newImage = new byte[image.length];
235+
236+
double imageLength = image.length;
237+
// see comment in `invertColors_vectorized`
238+
int loopBound = RGB_SPECIES.loopBound(image.length) - RGB_STEPS;
239+
int pixel = 0;
240+
// vectorized loop
241+
for (; pixel < loopBound; pixel += RGB_STEPS) {
242+
// Deviating from the classic loop, the quotient is not computed for each pixel,
243+
// but for each "pixel block" of length `RGB_STEPS`. This means the resulting image
244+
// differs from the one produced by the classic loop and also across different
245+
// CPU architectures with different species lengths.
246+
double purpleQuotient = (pixel / imageLength);
247+
byte purpleIndex = (byte) (255 * purpleQuotient);
248+
249+
var rgbValues = ByteVector.fromArray(RGB_SPECIES, image, pixel);
250+
var purpleRgbValues = (ByteVector) RGB_SPECIES
251+
.broadcast(0)
252+
.blend(purpleIndex, PURPLE_SHIFT);
253+
var purpleMask = rgbValues.compare(VectorOperators.UNSIGNED_LT, purpleRgbValues);
254+
var newRgbValues = rgbValues.blend(purpleRgbValues, purpleMask);
255+
256+
newRgbValues.intoArray(newImage, pixel);
257+
}
258+
// remainder
259+
for (; pixel + 2 < image.length; pixel += 3) {
260+
double purpleQuotient = (pixel / imageLength);
261+
byte purpleIndex = (byte) (255 * purpleQuotient);
262+
263+
byte blue = image[pixel];
264+
byte green = image[pixel + 1];
265+
byte red = image[pixel + 2];
266+
267+
// boost blue and red to tint purple
268+
byte newBlue = maxByte(purpleIndex, blue);
269+
byte newRed = maxByte(purpleIndex, red);
270+
271+
newImage[pixel] = newBlue;
272+
newImage[pixel + 1] = green;
273+
newImage[pixel + 2] = newRed;
274+
}
275+
276+
return newImage;
277+
}
278+
279+
private static byte maxByte(byte x, byte y) {
280+
return Byte.compareUnsigned(x, y) > 0 ? x : y;
281+
}
282+
283+
}

Diff for: src/main/java/module-info.java

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
requires java.desktop;
1414
requires java.net.http;
15+
requires jdk.incubator.vector;
1516

1617
provides RandomGenerator with Xkcd;
1718
provides InetAddressResolverProvider with ForwardingInetAddressResolverProvider;

Diff for: src/main/resources/hcmc.jpg

4.33 MB
Loading

Diff for: src/main/resources/hk.jpg

1.78 MB
Loading

Diff for: src/main/resources/images.json

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[
2+
{
3+
"fileName": "hcmc",
4+
"source": "https://unsplash.com/photos/6p2AkWAIC8E",
5+
"artist": "https://unsplash.com/@jetjetdelacruz",
6+
"license": "https://unsplash.com/license"
7+
},
8+
{
9+
"fileName": "hk",
10+
"source": "https://unsplash.com/photos/C_q-e_aBRcA",
11+
"artist": "https://unsplash.com/@richardlee",
12+
"license": "https://unsplash.com/license"
13+
},
14+
{
15+
"fileName": "tokyo",
16+
"source": "https://unsplash.com/photos/7H77FWkK_x4",
17+
"artist": "https://unsplash.com/@jezar",
18+
"license": "https://unsplash.com/license"
19+
}
20+
]

Diff for: src/main/resources/tokyo.jpg

2.58 MB
Loading

0 commit comments

Comments
 (0)