perf: optimize texture rendering and scanlines
authorSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Fri, 20 Mar 2026 20:51:28 +0000 (22:51 +0200)
committerSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Fri, 20 Mar 2026 20:51:28 +0000 (22:51 +0200)
- Optimize scanline texture coordinate interpolation
- Optimize texture sampling and add alpha blending
- Optimize mipmap downsampling/upsampling with direct pixel ops
- Fix y-coordinate swap in drawRectangle
- Use Arrays.fill for fillColor and opaque scanlines

src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedPolygon.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/TexturedRectangle.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/Texture.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/TextureBitmap.java

index 124b679..d62c034 100644 (file)
@@ -99,9 +99,10 @@ public class SolidPolygon extends AbstractCoordinateShape {
         final int b = color.b;
 
         if (polygonAlpha == 255) {
-            final int pixel = (r << 16) | (g << 8) | b;
-            for (int i = 0; i < width; i++)
-                pixels[offset++] = pixel;
+            if (width > 0) {
+                final int pixel = (r << 16) | (g << 8) | b;
+                java.util.Arrays.fill(pixels, offset, offset + width, pixel);
+            }
         } else {
             final int backgroundAlpha = 255 - polygonAlpha;
 
index 8042b77..094ec42 100644 (file)
@@ -129,19 +129,57 @@ public class TexturedPolygon extends AbstractCoordinateShape {
         final double twidth = tx2 - tx1;
         final double theight = ty2 - ty1;
 
+        final double txStep = twidth / realWidth;
+        final double tyStep = theight / realWidth;
+
+        double tx = tx1 + txStep * (x1 - realX1);
+        double ty = ty1 + tyStep * (x1 - realX1);
+
+        final int[] texPixels = textureBitmap.pixels;
+        final int texW = textureBitmap.width;
+        final int texH = textureBitmap.height;
+        final int texWMinus1 = texW - 1;
+        final int texHMinus1 = texH - 1;
+
         for (int x = x1; x < x2; x++) {
 
-            final double distance = x - realX1;
+            int itx = (int) tx;
+            int ity = (int) ty;
+
+            if (itx < 0) itx = 0;
+            else if (itx > texWMinus1) itx = texWMinus1;
+
+            if (ity < 0) ity = 0;
+            else if (ity > texHMinus1) ity = texHMinus1;
+
+            final int srcPixel = texPixels[ity * texW + itx];
+            final int srcAlpha = (srcPixel >> 24) & 0xff;
+
+            if (srcAlpha != 0) {
+                if (srcAlpha == 255) {
+                    renderBufferPixels[renderBufferOffset] = srcPixel;
+                } else {
+                    final int backgroundAlpha = 255 - srcAlpha;
+
+                    final int srcR = (srcPixel >> 16) & 0xff;
+                    final int srcG = (srcPixel >> 8) & 0xff;
+                    final int srcB = srcPixel & 0xff;
 
-            final double tx = tx1 + ((twidth * distance) / realWidth);
-            final double ty = ty1 + ((theight * distance) / realWidth);
+                    final int destPixel = renderBufferPixels[renderBufferOffset];
+                    final int destR = (destPixel >> 16) & 0xff;
+                    final int destG = (destPixel >> 8) & 0xff;
+                    final int destB = destPixel & 0xff;
 
-            final int textureOffset = textureBitmap.getAddress((int) tx,
-                    (int) ty);
+                    final int r = ((destR * backgroundAlpha) + (srcR * srcAlpha)) / 256;
+                    final int g = ((destG * backgroundAlpha) + (srcG * srcAlpha)) / 256;
+                    final int b = ((destB * backgroundAlpha) + (srcB * srcAlpha)) / 256;
 
-            textureBitmap.drawPixel(textureOffset, renderBufferPixels,
-                    renderBufferOffset);
+                    renderBufferPixels[renderBufferOffset] = (r << 16) | (g << 8) | b;
+                }
+            }
 
+            tx += txStep;
+            ty += tyStep;
             renderBufferOffset++;
         }
 
index 1ababb2..aa52272 100644 (file)
@@ -177,9 +177,4 @@ public class TexturedRectangle extends AbstractCompositeShape {
         addShape(texturedPolygon2);
     }
 
-//    public void initialize(final int width, final int height,
-//                           final int maxTextureUpscale) {
-//        initialize(width, height, width, height, maxTextureUpscale);
-//    }
-
 }
index b650230..7558eb8 100644 (file)
@@ -171,18 +171,38 @@ public class Texture {
         final TextureBitmap downScaled = new TextureBitmap(newWidth, newHeight,
                 originalBitmap.multiplicationFactor / 2d);
 
-        final ColorAccumulator accumulator = new ColorAccumulator();
+        final int[] srcPixels = originalBitmap.pixels;
+        final int[] dstPixels = downScaled.pixels;
+        final int srcW = originalBitmap.width;
+        final int srcH = originalBitmap.height;
+        final int srcWMinus1 = srcW - 1;
+        final int srcHMinus1 = srcH - 1;
+
+        for (int y = 0; y < newHeight; y++) {
+            final int srcYBase = y * 2;
+            final int srcY1 = Math.min(srcYBase, srcHMinus1);
+            final int srcY2 = Math.min(srcYBase + 1, srcHMinus1);
+            final int row1Offset = srcY1 * srcW;
+            final int row2Offset = srcY2 * srcW;
 
-        for (int y = 0; y < newHeight; y++)
             for (int x = 0; x < newWidth; x++) {
-                accumulator.reset();
-                accumulator.accumulate(originalBitmap, x * 2, y * 2);
-                accumulator.accumulate(originalBitmap, (x * 2) + 1, y * 2);
-                accumulator.accumulate(originalBitmap, x * 2, (y * 2) + 1);
-                accumulator
-                        .accumulate(originalBitmap, (x * 2) + 1, (y * 2) + 1);
-                accumulator.storeResult(downScaled, x, y);
+                final int srcXBase = x * 2;
+                final int srcX1 = Math.min(srcXBase, srcWMinus1);
+                final int srcX2 = Math.min(srcXBase + 1, srcWMinus1);
+
+                final int p0 = srcPixels[row1Offset + srcX1];
+                final int p1 = srcPixels[row1Offset + srcX2];
+                final int p2 = srcPixels[row2Offset + srcX1];
+                final int p3 = srcPixels[row2Offset + srcX2];
+
+                final int a = (((p0 >>> 24) + (p1 >>> 24) + (p2 >>> 24) + (p3 >>> 24)) >> 2);
+                final int r = ((((p0 >> 16) & 0xff) + ((p1 >> 16) & 0xff) + ((p2 >> 16) & 0xff) + ((p3 >> 16) & 0xff)) >> 2);
+                final int g = ((((p0 >> 8) & 0xff) + ((p1 >> 8) & 0xff) + ((p2 >> 8) & 0xff) + ((p3 >> 8) & 0xff)) >> 2);
+                final int b = (((p0 & 0xff) + (p1 & 0xff) + (p2 & 0xff) + (p3 & 0xff)) >> 2);
+
+                dstPixels[y * newWidth + x] = (a << 24) | (r << 16) | (g << 8) | b;
             }
+        }
 
         return downScaled;
     }
@@ -273,41 +293,57 @@ public class Texture {
      * @return The upscaled bitmap
      */
     public TextureBitmap upscaleBitmap(final TextureBitmap originalBitmap) {
-        final int newWidth = originalBitmap.width * 2;
-        final int newHeight = originalBitmap.height * 2;
+        final int srcW = originalBitmap.width;
+        final int srcH = originalBitmap.height;
+        final int newWidth = srcW * 2;
+        final int newHeight = srcH * 2;
+        final int srcWMinus1 = srcW - 1;
+        final int srcHMinus1 = srcH - 1;
 
         final TextureBitmap upScaled = new TextureBitmap(newWidth, newHeight,
                 originalBitmap.multiplicationFactor * 2d);
 
-        final ColorAccumulator accumulator = new ColorAccumulator();
-
-        for (int y = 0; y < originalBitmap.height; y++)
-            for (int x = 0; x < originalBitmap.width; x++) {
-                accumulator.reset();
-                accumulator.accumulate(originalBitmap, x, y);
-                accumulator.storeResult(upScaled, x * 2, y * 2);
-
-                accumulator.reset();
-                accumulator.accumulate(originalBitmap, x, y);
-                accumulator.accumulate(originalBitmap, x + 1, y);
-                accumulator.storeResult(upScaled, (x * 2) + 1, y * 2);
-
-                accumulator.reset();
-                accumulator.accumulate(originalBitmap, x, y);
-                accumulator.accumulate(originalBitmap, x, y + 1);
-                accumulator.storeResult(upScaled, x * 2, (y * 2) + 1);
-
-                accumulator.reset();
-                accumulator.accumulate(originalBitmap, x, y);
-                accumulator.accumulate(originalBitmap, x + 1, y);
-                accumulator.accumulate(originalBitmap, x, y + 1);
-                accumulator.accumulate(originalBitmap, x + 1, y + 1);
-                accumulator.storeResult(upScaled, (x * 2) + 1, (y * 2) + 1);
+        final int[] src = originalBitmap.pixels;
+        final int[] dst = upScaled.pixels;
+
+        for (int y = 0; y < srcH; y++) {
+            final int srcRowOffset = y * srcW;
+            final int nextRowOffset = Math.min(y + 1, srcHMinus1) * srcW;
+            final int dstRow0Offset = (y * 2) * newWidth;
+            final int dstRow1Offset = (y * 2 + 1) * newWidth;
+
+            for (int x = 0; x < srcW; x++) {
+                final int nx = Math.min(x + 1, srcWMinus1);
+
+                final int p00 = src[srcRowOffset + x];
+                final int p10 = src[srcRowOffset + nx];
+                final int p01 = src[nextRowOffset + x];
+                final int p11 = src[nextRowOffset + nx];
+
+                dst[dstRow0Offset + x * 2] = p00;
+                dst[dstRow0Offset + x * 2 + 1] = avg2(p00, p10);
+                dst[dstRow1Offset + x * 2] = avg2(p00, p01);
+                dst[dstRow1Offset + x * 2 + 1] = avg4(p00, p10, p01, p11);
             }
+        }
 
         return upScaled;
     }
 
+    private static int avg2(final int p0, final int p1) {
+        return (((((p0 >>> 24) + (p1 >>> 24)) >> 1) << 24)
+                | (((((p0 >> 16) & 0xff) + ((p1 >> 16) & 0xff)) >> 1) << 16)
+                | (((((p0 >> 8) & 0xff) + ((p1 >> 8) & 0xff)) >> 1) << 8)
+                | (((p0 & 0xff) + (p1 & 0xff)) >> 1));
+    }
+
+    private static int avg4(final int p0, final int p1, final int p2, final int p3) {
+        return ((((p0 >>> 24) + (p1 >>> 24) + (p2 >>> 24) + (p3 >>> 24)) >> 2) << 24)
+                | (((((p0 >> 16) & 0xff) + ((p1 >> 16) & 0xff) + ((p2 >> 16) & 0xff) + ((p3 >> 16) & 0xff)) >> 2) << 16)
+                | (((((p0 >> 8) & 0xff) + ((p1 >> 8) & 0xff) + ((p2 >> 8) & 0xff) + ((p3 >> 8) & 0xff)) >> 2) << 8)
+                | (((p0 & 0xff) + (p1 & 0xff) + (p2 & 0xff) + (p3 & 0xff)) >> 2);
+    }
+
     /**
      * A helper class that accumulates color values for a given area of a bitmap.
      */
index 7adb04f..8726bc5 100644 (file)
@@ -160,7 +160,7 @@ public class TextureBitmap {
      * @param y2    the bottom y coordinate (exclusive)
      * @param color the fill color
      */
-    public void drawRectangle(int x1, final int y1, int x2, final int y2,
+    public void drawRectangle(int x1, int y1, int x2, int y2,
                               final Color color) {
 
         if (x1 > x2) {
@@ -170,9 +170,9 @@ public class TextureBitmap {
         }
 
         if (y1 > y2) {
-            final int tmp = x1;
-            x1 = x2;
-            x2 = tmp;
+            final int tmp = y1;
+            y1 = y2;
+            y2 = tmp;
         }
 
         for (int y = y1; y < y2; y++)
@@ -190,8 +190,7 @@ public class TextureBitmap {
      */
     public void fillColor(final Color color) {
         final int pixel = (color.a << 24) | (color.r << 16) | (color.g << 8) | color.b;
-        for (int i = 0; i < pixels.length; i++)
-            pixels[i] = pixel;
+        java.util.Arrays.fill(pixels, pixel);
     }
 
     /**