Drop unwired 2D noise kernels

- Delete NoiseKernels2D, ScalarNoiseKernels2D and VectorNoiseKernels2D after measuring the vector kernel at 0.07x scalar on 2-lane NEON with no production call site
- Delete the 2D noise parity test and the SimdSupport plumbing that selected between the two kernel sets
- State in the simd-bench README that its 2D noise classes are a candidate Iris does not ship
This commit is contained in:
Brian Neumann-Fopiano
2026-09-08 04:33:32 -04:00
parent 79af11c435
commit 45b9ff8e11
7 changed files with 15 additions and 436 deletions
@@ -1,8 +0,0 @@
package art.arcane.iris.util.simd;
public interface NoiseKernels2D {
String describe();
void simplexFractalFBM(long seed, int octaves, double frequency, double lacunarity, double gain,
double fractalBounding, double[] xs, double[] zs, double[] out, int length);
}
@@ -1,99 +0,0 @@
package art.arcane.iris.util.simd;
public final class ScalarNoiseKernels2D implements NoiseKernels2D {
static final double F2 = 0.5D * (Math.sqrt(3.0D) - 1.0D);
static final double G2 = (3.0D - Math.sqrt(3.0D)) / 6.0D;
static final long X_PRIME = 1619L;
static final long Y_PRIME = 31337L;
static final double[] GRAD_2D = {-1D, -1D, 1D, -1D, -1D, 1D, 1D, 1D, 0D, -1D, -1D, 0D, 0D, 1D, 1D, 0D};
@Override
public String describe() {
return "scalar";
}
static long fastFloor(double f) {
return f >= 0D ? (long) f : (long) f - 1L;
}
static double gradCoord2D(long seed, long x, long y, double xd, double yd) {
long hash = seed;
hash ^= X_PRIME * x;
hash ^= Y_PRIME * y;
hash = hash * hash * hash * 60493L;
hash = (hash >> 13) ^ hash;
int gradientIndex = ((int) hash & 7) << 1;
return (xd * GRAD_2D[gradientIndex]) + (yd * GRAD_2D[gradientIndex + 1]);
}
static double singleSimplex(long seed, double x, double y) {
double t = (x + y) * F2;
long i = fastFloor(x + t);
long j = fastFloor(y + t);
t = (i + j) * G2;
double x0 = x - (i - t);
double y0 = y - (j - t);
long i1;
long j1;
if (x0 > y0) {
i1 = 1L;
j1 = 0L;
} else {
i1 = 0L;
j1 = 1L;
}
double x1 = x0 - i1 + G2;
double y1 = y0 - j1 + G2;
double x2 = x0 - 1D + (2D * G2);
double y2 = y0 - 1D + (2D * G2);
double n0;
double n1;
double n2;
double a = 0.5D - x0 * x0 - y0 * y0;
if (a < 0D) {
n0 = 0D;
} else {
a *= a;
n0 = a * a * gradCoord2D(seed, i, j, x0, y0);
}
double b = 0.5D - x1 * x1 - y1 * y1;
if (b < 0D) {
n1 = 0D;
} else {
b *= b;
n1 = b * b * gradCoord2D(seed, i + i1, j + j1, x1, y1);
}
double c = 0.5D - x2 * x2 - y2 * y2;
if (c < 0D) {
n2 = 0D;
} else {
c *= c;
n2 = c * c * gradCoord2D(seed, i + 1L, j + 1L, x2, y2);
}
return 50D * (n0 + n1 + n2);
}
static double simplexFractalFBMScalar(long seed, int octaves, double frequency, double lacunarity, double gain,
double fractalBounding, double xIn, double yIn) {
double x = xIn * frequency;
double y = yIn * frequency;
long s = seed;
double sum = singleSimplex(s, x, y);
double amp = 1D;
for (int o = 1; o < octaves; o++) {
x *= lacunarity;
y *= lacunarity;
amp *= gain;
sum += singleSimplex(++s, x, y) * amp;
}
return sum * fractalBounding;
}
@Override
public void simplexFractalFBM(long seed, int octaves, double frequency, double lacunarity, double gain,
double fractalBounding, double[] xs, double[] zs, double[] out, int length) {
for (int k = 0; k < length; k++) {
out[k] = simplexFractalFBMScalar(seed, octaves, frequency, lacunarity, gain, fractalBounding, xs[k], zs[k]);
}
}
}
@@ -9,9 +9,7 @@ public final class SimdSupport {
private static final String VECTOR_MODULE = "jdk.incubator.vector";
private static final boolean MODULE_PRESENT = ModuleLayer.boot().findModule(VECTOR_MODULE).isPresent();
private static final AtomicReference<Throwable> KERNEL_FAILURE = new AtomicReference<>();
private static final AtomicReference<Throwable> NOISE_KERNEL_FAILURE = new AtomicReference<>();
private static final SimdKernels KERNELS = selectKernels();
private static final NoiseKernels2D NOISE_KERNELS_2D = selectNoiseKernels2D();
private SimdSupport() {
}
@@ -20,10 +18,6 @@ public final class SimdSupport {
return KERNELS;
}
public static NoiseKernels2D noiseKernels2D() {
return NOISE_KERNELS_2D;
}
public static boolean isVectorModulePresent() {
return MODULE_PRESENT;
}
@@ -36,19 +30,10 @@ public final class SimdSupport {
return KERNEL_FAILURE.get();
}
public static Throwable noiseKernelInitializationFailure() {
return NOISE_KERNEL_FAILURE.get();
}
public static String kernelStatus() {
return kernelStatus(isVectorized(), KERNELS.describe(), MODULE_PRESENT, KERNEL_FAILURE.get(), simdEnabledInSettings());
}
public static String noiseKernelStatus() {
return noiseKernelStatus(!(NOISE_KERNELS_2D instanceof ScalarNoiseKernels2D), NOISE_KERNELS_2D.describe(),
MODULE_PRESENT, NOISE_KERNEL_FAILURE.get(), simdEnabledInSettings());
}
static String kernelStatus(boolean vectorized, String description, boolean modulePresent, Throwable failure, boolean settingEnabled) {
if (vectorized) {
return "vector kernels enabled (" + description + ")";
@@ -69,39 +54,13 @@ public final class SimdSupport {
return "scalar kernels active; the Vector API reported no usable vector shape on this CPU";
}
static String noiseKernelStatus(boolean vectorized, String description, boolean modulePresent, Throwable failure, boolean settingEnabled) {
if (vectorized) {
return "2D noise vector kernels enabled (" + description + ")";
}
if (!modulePresent) {
return "2D noise scalar kernels active; the " + VECTOR_MODULE + " module is not on this JVM";
}
if (failure != null) {
return "2D noise scalar kernels active; vector kernel initialization failed: " + describe(failure);
}
if (!settingEnabled) {
return "2D noise vector kernels disabled (performance.simdKernels=false)";
}
return "2D noise scalar kernels active; this CPU has fewer than the 4 double lanes the vector kernel needs to pay for itself";
}
public static void install() {
IrisLogging.info("SIMD: " + kernelStatus());
IrisLogging.info("SIMD: " + noiseKernelStatus());
Throwable failure = KERNEL_FAILURE.get();
if (failure != null) {
IrisLogging.reportError("Iris could not initialize its Vector API kernels; generation runs on the scalar kernels instead.", failure);
}
Throwable noiseFailure = NOISE_KERNEL_FAILURE.get();
if (noiseFailure != null) {
IrisLogging.reportError("Iris could not initialize its Vector API 2D noise kernels; noise runs on the scalar kernels instead.", noiseFailure);
}
}
public static SimdKernels createVectorKernels() {
@@ -117,24 +76,6 @@ public final class SimdSupport {
}
}
public static NoiseKernels2D createVectorNoiseKernels2D() {
if (!MODULE_PRESENT) {
return null;
}
try {
Class<?> cls = Class.forName("art.arcane.iris.util.simd.VectorNoiseKernels2D");
boolean profitable = (boolean) cls.getMethod("profitable").invoke(null);
if (!profitable) {
return null;
}
return (NoiseKernels2D) cls.getDeclaredConstructor().newInstance();
} catch (Throwable e) {
NOISE_KERNEL_FAILURE.set(e);
return null;
}
}
private static SimdKernels selectKernels() {
if (!simdEnabledInSettings()) {
return new ScalarSimdKernels();
@@ -144,15 +85,6 @@ public final class SimdSupport {
return vector == null ? new ScalarSimdKernels() : vector;
}
private static NoiseKernels2D selectNoiseKernels2D() {
if (!simdEnabledInSettings()) {
return new ScalarNoiseKernels2D();
}
NoiseKernels2D vector = createVectorNoiseKernels2D();
return vector == null ? new ScalarNoiseKernels2D() : vector;
}
private static String describe(Throwable failure) {
Throwable root = failure;
while (root.getCause() != null && root.getCause() != root) {
@@ -1,129 +0,0 @@
package art.arcane.iris.util.simd;
import jdk.incubator.vector.DoubleVector;
import jdk.incubator.vector.LongVector;
import jdk.incubator.vector.VectorMask;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
public final class VectorNoiseKernels2D implements NoiseKernels2D {
private static final VectorSpecies<Double> DS = DoubleVector.SPECIES_PREFERRED;
private static final VectorSpecies<Long> LS = LongVector.SPECIES_PREFERRED;
private static final boolean ALIGNED = DS.length() == LS.length();
private static final int MIN_PROFITABLE_LANES = 4;
private static final boolean PROFITABLE = ALIGNED && DS.length() >= MIN_PROFITABLE_LANES;
private static final double[] GRAD_X = {-1D, 1D, -1D, 1D, 0D, -1D, 0D, 1D};
private static final double[] GRAD_Y = {-1D, -1D, 1D, 1D, -1D, 0D, 1D, 0D};
private static final double F2 = ScalarNoiseKernels2D.F2;
private static final double G2 = ScalarNoiseKernels2D.G2;
private static final long X_PRIME = ScalarNoiseKernels2D.X_PRIME;
private static final long Y_PRIME = ScalarNoiseKernels2D.Y_PRIME;
private static final ThreadLocal<long[]> LONG_SCRATCH = ThreadLocal.withInitial(() -> new long[LS.length()]);
public static boolean lanesAligned() {
return ALIGNED;
}
public static boolean profitable() {
return PROFITABLE;
}
@Override
public String describe() {
return DS.length() + "x64 lanes, " + DS.vectorShape();
}
private static LongVector floorToLong(DoubleVector f) {
LongVector truncated = (LongVector) f.convertShape(VectorOperators.D2L, LS, 0);
VectorMask<Long> negative = f.compare(VectorOperators.LT, 0D).cast(LS);
return truncated.sub(1L, negative);
}
private static DoubleVector toDouble(LongVector v) {
return (DoubleVector) v.convertShape(VectorOperators.L2D, DS, 0);
}
private static DoubleVector gradCoord(long seed, LongVector i, LongVector j, DoubleVector xd, DoubleVector yd,
int[] idxScratch) {
LongVector hash = LongVector.broadcast(LS, seed)
.lanewise(VectorOperators.XOR, i.mul(X_PRIME))
.lanewise(VectorOperators.XOR, j.mul(Y_PRIME));
hash = hash.mul(hash).mul(hash).mul(60493L);
LongVector shifted = hash.lanewise(VectorOperators.ASHR, 13);
hash = shifted.lanewise(VectorOperators.XOR, hash);
LongVector idx = hash.lanewise(VectorOperators.AND, 7L);
long[] tmp = LONG_SCRATCH.get();
idx.intoArray(tmp, 0);
for (int l = 0; l < idxScratch.length; l++) {
idxScratch[l] = (int) tmp[l];
}
DoubleVector gx = DoubleVector.fromArray(DS, GRAD_X, 0, idxScratch, 0);
DoubleVector gy = DoubleVector.fromArray(DS, GRAD_Y, 0, idxScratch, 0);
return xd.mul(gx).add(yd.mul(gy));
}
private static DoubleVector corner(DoubleVector xk, DoubleVector yk, DoubleVector grad) {
DoubleVector t = DoubleVector.broadcast(DS, 0.5D).sub(xk.mul(xk)).sub(yk.mul(yk));
VectorMask<Double> negative = t.compare(VectorOperators.LT, 0D);
DoubleVector t2 = t.mul(t);
DoubleVector t4 = t2.mul(t2);
return t4.mul(grad).blend(0D, negative);
}
private static DoubleVector singleSimplexVector(long seed, DoubleVector x, DoubleVector y, int[] idxScratch) {
DoubleVector t = x.add(y).mul(F2);
LongVector i = floorToLong(x.add(t));
LongVector j = floorToLong(y.add(t));
DoubleVector skew = toDouble(i.add(j)).mul(G2);
DoubleVector x0 = x.sub(toDouble(i).sub(skew));
DoubleVector y0 = y.sub(toDouble(j).sub(skew));
VectorMask<Double> xGreater = x0.compare(VectorOperators.GT, y0);
VectorMask<Long> xGreaterL = xGreater.cast(LS);
LongVector i1 = LongVector.zero(LS).blend(1L, xGreaterL);
LongVector j1 = LongVector.broadcast(LS, 1L).blend(0L, xGreaterL);
DoubleVector x1 = x0.sub(toDouble(i1)).add(G2);
DoubleVector y1 = y0.sub(toDouble(j1)).add(G2);
DoubleVector x2 = x0.sub(1D).add(2D * G2);
DoubleVector y2 = y0.sub(1D).add(2D * G2);
DoubleVector n0 = corner(x0, y0, gradCoord(seed, i, j, x0, y0, idxScratch));
DoubleVector n1 = corner(x1, y1, gradCoord(seed, i.add(i1), j.add(j1), x1, y1, idxScratch));
DoubleVector n2 = corner(x2, y2, gradCoord(seed, i.add(1L), j.add(1L), x2, y2, idxScratch));
return n0.add(n1).add(n2).mul(50D);
}
@Override
public void simplexFractalFBM(long seed, int octaves, double frequency, double lacunarity, double gain,
double fractalBounding, double[] xs, double[] zs, double[] out, int length) {
if (!ALIGNED) {
tailScalar(seed, octaves, frequency, lacunarity, gain, fractalBounding, xs, zs, out, 0, length);
return;
}
int lanes = DS.length();
int bound = DS.loopBound(length);
int[] idxScratch = new int[lanes];
int k = 0;
for (; k < bound; k += lanes) {
DoubleVector x = DoubleVector.fromArray(DS, xs, k).mul(frequency);
DoubleVector y = DoubleVector.fromArray(DS, zs, k).mul(frequency);
long s = seed;
DoubleVector sum = singleSimplexVector(s, x, y, idxScratch);
double amp = 1D;
for (int o = 1; o < octaves; o++) {
x = x.mul(lacunarity);
y = y.mul(lacunarity);
amp *= gain;
sum = sum.add(singleSimplexVector(++s, x, y, idxScratch).mul(amp));
}
sum.mul(fractalBounding).intoArray(out, k);
}
tailScalar(seed, octaves, frequency, lacunarity, gain, fractalBounding, xs, zs, out, k, length);
}
private static void tailScalar(long seed, int octaves, double frequency, double lacunarity, double gain,
double fractalBounding, double[] xs, double[] zs, double[] out, int from, int length) {
for (int k = from; k < length; k++) {
out[k] = ScalarNoiseKernels2D.simplexFractalFBMScalar(seed, octaves, frequency, lacunarity, gain,
fractalBounding, xs[k], zs[k]);
}
}
}
@@ -1,120 +0,0 @@
package art.arcane.iris.util.simd;
import art.arcane.iris.util.project.noise.FastNoiseDouble;
import art.arcane.iris.util.project.noise.FastNoiseDouble.FractalType;
import art.arcane.iris.util.project.noise.FastNoiseDouble.NoiseType;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class NoiseKernels2DParityTest {
private static FastNoiseDouble oracle(long seed, int octaves) {
FastNoiseDouble n = new FastNoiseDouble(seed);
n.setNoiseType(NoiseType.SimplexFractal);
n.setFractalType(FractalType.FBM);
n.setFractalOctaves(octaves);
return n;
}
private static void assertKernelMatchesOracle(NoiseKernels2D kernel) {
RNG rng = new RNG(99L);
for (int octaves = 1; octaves <= 8; octaves++) {
FastNoiseDouble n = oracle(1337L + octaves, octaves);
int length = 256;
double[] xs = new double[length];
double[] zs = new double[length];
double[] expected = new double[length];
double[] out = new double[length];
for (int k = 0; k < length; k++) {
double x = (rng.nextDouble() - 0.5D) * 1_000_000D;
double z = (rng.nextDouble() - 0.5D) * 1_000_000D;
xs[k] = x;
zs[k] = z;
expected[k] = n.GetSimplexFractal(x, z);
}
kernel.simplexFractalFBM(1337L + octaves, octaves, n.getFrequency(), 2.0D, 0.5D, n.getFractalBounding(), xs, zs, out, length);
for (int k = 0; k < length; k++) {
assertEquals("octaves=" + octaves + " idx=" + k, expected[k], out[k], 0D);
}
}
}
@Test
public void scalarKernelIsBitExactToOracle() {
assertKernelMatchesOracle(new ScalarNoiseKernels2D());
}
@Test
public void vectorKernelSingleOctaveIsBitExactToOracle() {
org.junit.Assume.assumeTrue(VectorNoiseKernels2D.lanesAligned());
FastNoiseDouble n = oracle(202020L, 1);
RNG rng = new RNG(7L);
int length = 333;
double[] xs = new double[length];
double[] zs = new double[length];
double[] expected = new double[length];
double[] out = new double[length];
for (int k = 0; k < length; k++) {
double x = (rng.nextDouble() - 0.5D) * 800_000D;
double z = (rng.nextDouble() - 0.5D) * 800_000D;
xs[k] = x;
zs[k] = z;
expected[k] = n.GetSimplexFractal(x, z);
}
new VectorNoiseKernels2D().simplexFractalFBM(202020L, 1, n.getFrequency(), 2.0D, 0.5D, n.getFractalBounding(), xs, zs, out, length);
for (int k = 0; k < length; k++) {
assertEquals("idx=" + k, expected[k], out[k], 0D);
}
}
@Test
public void vectorKernelAllOctavesAreBitExactToOracle() {
org.junit.Assume.assumeTrue(VectorNoiseKernels2D.lanesAligned());
assertKernelMatchesOracle(new VectorNoiseKernels2D());
}
@Test
public void simdSupportReturnsBitExactNoiseKernel() {
NoiseKernels2D kernel = SimdSupport.noiseKernels2D();
org.junit.Assert.assertNotNull(kernel);
assertKernelMatchesOracle(kernel);
}
@Test
public void simdSupportNeverSelectsVectorBelowProfitableLanes() {
if (VectorNoiseKernels2D.profitable()) {
org.junit.Assert.assertTrue(SimdSupport.createVectorNoiseKernels2D() instanceof VectorNoiseKernels2D);
} else {
org.junit.Assert.assertTrue(SimdSupport.noiseKernels2D() instanceof ScalarNoiseKernels2D);
}
}
@Test
public void vectorMatchesScalarAcrossEdgeCases() {
org.junit.Assume.assumeTrue(VectorNoiseKernels2D.lanesAligned());
NoiseKernels2D scalar = new ScalarNoiseKernels2D();
NoiseKernels2D vector = new VectorNoiseKernels2D();
RNG rng = new RNG(31337L);
int[] lengths = {1, 3, 7, 8, 9, 255, 256, 257};
double[] edgeCoords = {-4.0D, -3.0D, -1.0D, -0.0D, 0.0D, 1.0D, 4.0D, 1_000_000.0D, -1_000_000.0D};
for (int octaves = 1; octaves <= 8; octaves++) {
for (int length : lengths) {
double[] xs = new double[length];
double[] zs = new double[length];
double[] a = new double[length];
double[] b = new double[length];
for (int k = 0; k < length; k++) {
boolean edge = (k % 3) == 0;
xs[k] = edge ? edgeCoords[k % edgeCoords.length] : (rng.nextDouble() - 0.5D) * 2_000_000D;
zs[k] = edge ? edgeCoords[(k + 1) % edgeCoords.length] : (rng.nextDouble() - 0.5D) * 2_000_000D;
}
scalar.simplexFractalFBM(77L, octaves, 0.01D, 2.0D, 0.5D, 0.6667D, xs, zs, a, length);
vector.simplexFractalFBM(77L, octaves, 0.01D, 2.0D, 0.5D, 0.6667D, xs, zs, b, length);
for (int k = 0; k < length; k++) {
assertEquals("oct=" + octaves + " len=" + length + " idx=" + k, a[k], b[k], 0D);
}
}
}
}
}
@@ -88,20 +88,19 @@ public class SimdSupportStatusTest {
}
@Test
public void aNarrowCpuIsReportedAsUnprofitableRatherThanDisabled() {
String status = SimdSupport.noiseKernelStatus(false, "scalar", true, null, true);
public void aCpuWithoutAUsableVectorShapeIsReportedAsSuchRatherThanDisabled() {
String status = SimdSupport.kernelStatus(false, "scalar", true, null, true);
assertTrue(status, status.contains("4 double lanes"));
assertTrue(status, status.contains("no usable vector shape"));
assertFalse(status, status.contains("performance.simdKernels=false"));
}
@Test
public void installStatesTheLiveKernelSelectionForBothKernelSets() {
public void installStatesTheLiveKernelSelection() {
SimdSupport.install();
assertTrue(emitted.toString(), emitted.size() >= 2);
assertTrue(emitted.toString(), emitted.size() >= 1);
assertTrue(emitted.toString(), emitted.contains("SIMD: " + SimdSupport.kernelStatus()));
assertTrue(emitted.toString(), emitted.contains("SIMD: " + SimdSupport.noiseKernelStatus()));
}
@Test
@@ -111,6 +110,5 @@ public class SimdSupportStatusTest {
}
assertFalse(SimdSupport.kernelStatus(), SimdSupport.kernelStatus().contains("performance.simdKernels=false"));
assertFalse(SimdSupport.noiseKernelStatus(), SimdSupport.noiseKernelStatus().contains("performance.simdKernels=false"));
}
}
+10 -5
View File
@@ -6,9 +6,13 @@ on**. The Volmit dev/test machines are Apple Silicon, where the wide-SIMD path
(4+ double lanes) does not exist. Copy the built artifact to a Windows/x86 box
(AVX2 = 4 lanes, AVX-512 = 8 lanes) and run it to get real numbers for that CPU.
The six kernel classes are copied verbatim (logic byte-for-byte) from
`Iris/core/.../util/simd/`. This tool is intentionally a duplicate so it builds
and runs on its own with no Gradle, no VolmLib, and no Iris on the classpath.
The three array kernel classes are copied verbatim (logic byte-for-byte) from
`Iris/core/.../util/simd/`. The three 2D noise kernel classes are a candidate
implementation that Iris does not ship: it measured 0.07x against scalar on
2-lane NEON and was removed, and this harness keeps it so the same question can be
answered on a wider CPU before anyone rebuilds it. This tool is intentionally a
duplicate so it builds and runs on its own with no Gradle, no VolmLib, and no Iris
on the classpath.
## Requirements
@@ -98,7 +102,8 @@ run.bat --mode both (default)
throughput will be (cache behavior, allocation, and surrounding code differ in
the real engine).
- **Effort 1 array kernels** (`roundToInt` / `sum` / `max`) run unconditionally
in real Iris. **Effort 2 noise** (`simplexFractalFBM`) is currently unwired in
Iris and gated to 4+ double lanes; this tool force-measures it regardless.
in real Iris. **Effort 2 noise** (`simplexFractalFBM`) is not part of
Iris; this tool force-measures the candidate kernel regardless of the 4+ double
lane gate.
- Vector-API auto-vectorization and cost depend heavily on the JDK version and
CPU. Run on the actual target hardware; do not extrapolate across machines.