feat: vectorize image processing with Java Vector API#191
Conversation
…vector) - Enable jdk.incubator.vector in module-info.java, build.gradle compile options, test JVM args, and native-image buildArgs - ArrayImageRGB.alphaBlendOver: vectorized inner blend loop using IntVector (8 lanes on AVX2, 4 on NEON) with scalar tail fallback - ScaleImageUtils: vectorized Mitchell-Netravali [R,G,B,W] accumulation using DoubleVector in both resampleHorizontal and resampleVertical - Rgb.distanceSquaredSum: new SIMD batch method summing squared RGB distances across a pixel array using IntVector - UnicodeGlyphImage: use Rgb.distanceSquaredSum in computeRegionStdDev and computeFullBlockStdDev; update CC0 header to Apache 2.0 Co-authored-by: crramirez <8397379+crramirez@users.noreply.github.com>
Co-authored-by: crramirez <8397379+crramirez@users.noreply.github.com>
Co-authored-by: crramirez <8397379+crramirez@users.noreply.github.com>
|
@copilot you added a new bundle, remember to add it also to the native jsons for its native image inclusion |
Co-authored-by: crramirez <8397379+crramirez@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR introduces Java Vector API–accelerated kernels for image processing in Casciian’s bits package, and wires in a demo UI to compare vectorized vs scalar baselines. It also updates build/native-image settings so the incubator Vector module is available during compilation, tests, and GraalVM native builds.
Changes:
- Vectorized core image kernels (alpha blending and RGB distance-sum) and updated call sites to use the new SIMD kernel.
- Added a demo “Vector performance” window + localized resource bundles and native-image resource configuration.
- Updated module/build/native-image configuration to include
jdk.incubator.vector.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| code/src/main/resources/META-INF/native-image/io.crramirez/casciian-demo/resource-config.json | Registers the new demo resource bundle for native-image builds. |
| code/src/main/resources/demo/DemoVectorPerformanceWindowBundle.properties | Adds English strings for the new vector performance demo window. |
| code/src/main/resources/demo/DemoVectorPerformanceWindowBundle_es.properties | Adds Spanish strings for the new vector performance demo window. |
| code/src/main/resources/demo/DemoMainWindowBundle.properties | Adds labels/buttons to open the new performance comparison window. |
| code/src/main/resources/demo/DemoMainWindowBundle_es.properties | Spanish localization for the new main-window entries. |
| code/src/main/resources/demo/DemoApplicationBundle.properties | Adds a menu item to launch the vector performance window. |
| code/src/main/resources/demo/DemoApplicationBundle_es.properties | Spanish localization for the new menu item. |
| code/src/main/java/module-info.java | Declares a module dependency on jdk.incubator.vector. |
| code/src/main/java/demo/DemoVectorPerformanceWindow.java | New demo UI window that benchmarks vector vs scalar kernels. |
| code/src/main/java/demo/DemoMainWindow.java | Hooks the new demo window into the main demo UI. |
| code/src/main/java/demo/DemoApplication.java | Adds a demo menu item and handler to open the new window. |
| code/src/main/java/casciian/bits/UnicodeGlyphImage.java | Switches stddev computation to use the new distance-sum kernel (with new flattening/allocation behavior). |
| code/src/main/java/casciian/bits/ScaleImageUtils.java | Introduces a Vector API path for convolution accumulation in resampling. |
| code/src/main/java/casciian/bits/Rgb.java | Adds distanceSquaredSum(...) SIMD kernel. |
| code/src/main/java/casciian/bits/ArrayImageRGB.java | Vectorizes alphaBlendOver(...) via a SIMD row kernel. |
| code/build.gradle | Adds --add-modules jdk.incubator.vector for compile/test/native-image. |
Suppressed comments (5)
code/src/main/java/casciian/bits/ScaleImageUtils.java:112
- This block declares sumR/sumG/sumB/sumW and then immediately resets them to 0 again; the second assignment is redundant and makes the inner loop harder to read.
double sumR = 0, sumG = 0, sumB = 0, sumW = 0;
sumR = 0; sumG = 0; sumB = 0; sumW = 0;
code/src/main/java/casciian/bits/ScaleImageUtils.java:126
- The vectorized accumulation currently allocates new double[] arrays inside the tap loop (one per source sample). This turns the hot inner loop into an allocation-heavy path and can easily dominate runtime/GC, masking any SIMD benefit.
// [R, G, B, W] * weight (pad with 0 for lanes > 4)
double[] contrib = new double[laneCount];
contrib[0] = ((pixel >>> 16) & 0xFF) * weight;
code/src/main/java/casciian/bits/ScaleImageUtils.java:194
- Same issue in the vertical pass: allocating a new contrib/result array inside the tap loop makes the SIMD path allocation-bound. Reusing a single scratch array per output pixel avoids O(kernelWidth) allocations per destination pixel.
double[] contrib = new double[laneCount];
contrib[0] = ((pixel >>> 16) & 0xFF) * weight;
contrib[1] = ((pixel >>> 8) & 0xFF) * weight;
contrib[2] = ( pixel & 0xFF) * weight;
contrib[3] = weight;
acc = acc.add(DoubleVector.fromArray(DOUBLE_SPECIES, contrib, 0));
code/src/main/java/casciian/bits/ScaleImageUtils.java:49
- The comment claims AVX2 does a fused-multiply-add (FMA) per pixel, but the implementation uses scalar multiplies into a temporary array plus a vector add (no FMA). This is misleading for future maintainers trying to reason about the vectorization strategy.
* Preferred {@code double} vector species for SIMD convolution.
* On x86 with AVX2 this is 4 lanes; on ARM NEON it is 2 lanes.
* The channel layout used in the kernel is [R, G, B, W] — exactly
* 4 doubles — so AVX2 processes one pixel's full kernel contribution
* in a single fused-multiply-add step.
code/src/main/java/demo/DemoVectorPerformanceWindow.java:335
- This file defines a private LongSupplier even though the JDK already provides java.util.function.LongSupplier. Using the JDK type would reduce custom surface area and avoid confusing readers who expect the standard functional interface.
@FunctionalInterface
private interface LongSupplier {
long getAsLong();
}
| public static long distanceSquaredSum(final int[] pixels, | ||
| final int count, | ||
| final int color) { | ||
| final VectorSpecies<Integer> species = IntVector.SPECIES_PREFERRED; | ||
| final int laneCount = species.length(); | ||
|
|
| // Separate the pixels into two regions so we can call the vectorized | ||
| // distanceSquaredSum on each contiguous array. | ||
| int splitFore = leftRight ? (width / 2) * height : width * (height / 2); | ||
| int[] forePixels = new int[splitFore]; | ||
| int[] backPixels = new int[pixelCount - splitFore]; | ||
| int fi = 0, bi = 0; |
|
@copilot Fix the code for all comments in this review thread. When a review comment includes a suggested change, apply the suggestion exactly. Do not make changes beyond what is described in the linked review thread. |
Co-authored-by: crramirez <8397379+crramirez@users.noreply.github.com>
Addressed in cb4f313: removed the unused imports, added direct tests for |
|
@copilot this screenshot was taken from a native build and this running the jar in java the first time
and this after ten times
|
Co-authored-by: crramirez <8397379+crramirez@users.noreply.github.com>
Addressed in d05d930. I added native-image-aware scalar fallbacks for |
… harness Co-authored-by: crramirez <8397379+crramirez@users.noreply.github.com>
Co-authored-by: crramirez <8397379+crramirez@users.noreply.github.com>
…or` for specific modules and adjust native build arguments to enable fallback compilation.



Summary
Testing