Hoist per-triangle mipmap metrics out of the per-tile paint path
authorSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Sun, 20 Sep 2026 08:17:58 +0000 (11:17 +0300)
committerSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Sun, 20 Sep 2026 08:17:58 +0000 (11:17 +0300)
- paintFlat no longer recomputes the screen-edge perimeter (3 sqrt) per
  overlapped tile: callers pass it in. Object path already computed it
  in paintTriangle (it was computed twice per paint); mesh handles read
  a per-slot value computed once in TriangleMeshBlock.transform (queued
  triangles only). Clipped fan sub-triangles compute inline (rare).
- TriangleMeshBlock UV perimeter (origTtd) precomputed once at build
  into uvPerimeter[] instead of 3 sqrt over the final uv[] per paint.

Bit-exact: same expressions, same accumulation order, same inputs —
verified by identical golden diff counts against the pre-change run
(23/23 scenes).

src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/MeshTriangle.java
src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.java
src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TriangleMeshBlock.java

index d62fa3b..9e040eb 100644 (file)
@@ -86,12 +86,25 @@ final class MeshTriangle extends TexturedTriangle {
             for (int i = 1; i + 1 < count; i++) {
                 loadClipVertex(screen[1], uvs[1], store, clip + i * 7);
                 loadClipVertex(screen[2], uvs[2], store, clip + (i + 1) * 7);
+                // Fan sub-triangle screen perimeter — same expression as
+                // the object path (edge12 + edge13 + edge23); straddlers
+                // are rare, so it is computed here rather than stored.
+                final double dx01 = screen[0].x - screen[1].x;
+                final double dy01 = screen[0].y - screen[1].y;
+                final double dx02 = screen[0].x - screen[2].x;
+                final double dy02 = screen[0].y - screen[2].y;
+                final double dx12 = screen[1].x - screen[2].x;
+                final double dy12 = screen[1].y - screen[2].y;
+                final double visPerimeter = Math.sqrt(dx01 * dx01 + dy01 * dy01)
+                        + Math.sqrt(dx02 * dx02 + dy02 * dy02)
+                        + Math.sqrt(dx12 * dx12 + dy12 * dy12);
                 paintFlat(renderBuffer, block.texture(index), block.backfaceCull(),
                         screen[0], screen[1], screen[2],
                         uvs[0], uvs[1], uvs[2],
                         store[clip + 2],
                         store[clip + i * 7 + 2],
                         store[clip + (i + 1) * 7 + 2],
+                        visPerimeter,
                         block.clipTtd(slot, clip));
             }
             return;
@@ -106,7 +119,8 @@ final class MeshTriangle extends TexturedTriangle {
                 block.camZ(slot, index, 0),
                 block.camZ(slot, index, 1),
                 block.camZ(slot, index, 2),
-                block.origTtd(index));
+                block.screenPerim(slot, index),
+                block.uvPerimeter(index));
     }
 
     /**
index 7d46c34..648baec 100644 (file)
@@ -512,6 +512,7 @@ public class TexturedTriangle extends AbstractCoordinateShape {
                 v1.transformedCoordinate(renderBuffer).z,
                 v2.transformedCoordinate(renderBuffer).z,
                 v3.transformedCoordinate(renderBuffer).z,
+                totalVisibleDistance,
                 totalTextureDistance);
     }
 
@@ -539,6 +540,11 @@ public class TexturedTriangle extends AbstractCoordinateShape {
      * @param z1               camera-space depth of vertex 1
      * @param z2               camera-space depth of vertex 2
      * @param z3               camera-space depth of vertex 3
+     * @param totalVisibleDistance screen-edge perimeter for mipmap
+     *                             selection, computed once per paint call
+     *                             (or once per slot for mesh blocks) —
+     *                             callers share it instead of the core
+     *                             recomputing per tile
      * @param totalTextureDistance UV perimeter for mipmap selection. For a
      *                             near-plane-clipped fan this is the
      *                             ORIGINAL triangle's perimeter (the clip
@@ -555,6 +561,7 @@ public class TexturedTriangle extends AbstractCoordinateShape {
                    final Point2D texturePoint2,
                    final Point2D texturePoint3,
                    final double z1, final double z2, final double z3,
+                   final double totalVisibleDistance,
                    final double totalTextureDistance) {
 
         // Z-buffer two-pass classification: opaque-class triangles
@@ -618,11 +625,6 @@ public class TexturedTriangle extends AbstractCoordinateShape {
             throw new IllegalStateException(
                     "SDF textures are not supported in mesh blocks");
 
-        final double edge12 = projectedPoint1.getDistanceTo(projectedPoint2);
-        final double edge13 = projectedPoint1.getDistanceTo(projectedPoint3);
-        final double edge23 = projectedPoint2.getDistanceTo(projectedPoint3);
-        final double totalVisibleDistance = edge12 + edge13 + edge23;
-
         final double scaleFactor = (totalVisibleDistance / totalTextureDistance) * 1.2d;
 
         final TextureBitmap mipmap = texture.getMipmapForScale(scaleFactor);
index 6084ee3..94d8195 100644 (file)
@@ -67,6 +67,17 @@ public final class TriangleMeshBlock extends AbstractShape {
     private final int[][] clipRef = new int[3][];
     private final double[][] clipStore = new double[3][];
     private final double[][] clipTtd = new double[3][];
+
+    /** Per-triangle UV perimeter (mipmap metric), computed once at build:
+     * the uv array never changes afterwards. Same expression and
+     * accumulation order as {@code TexturedTriangle}'s
+     * computeTotalTextureDistance: d(0,1) + d(0,2) + d(1,2). */
+    private final double[] uvPerimeter;
+
+    /** Per-triangle screen-edge perimeter (mipmap metric's visible side),
+     * computed once per slot in the transform loop — identical expression
+     * to what the paint core used to recompute per tile. */
+    private final double[][] screenPerim = new double[3][];
     private final int[] clipUsed = new int[3];
 
     // Scratch for the composed top transform of the current transform call.
@@ -112,6 +123,22 @@ public final class TriangleMeshBlock extends AbstractShape {
             java.util.Arrays.fill(clipRef[s], -1);
             clipStore[s] = new double[256];
             clipTtd[s] = new double[16];
+            screenPerim[s] = new double[triCount];
+        }
+
+        uvPerimeter = new double[triCount];
+        for (int t = 0; t < triCount; t++) {
+            final int u = t * 6;
+            final double d1 = Math.sqrt(
+                    ((uv[u] - uv[u + 2]) * (uv[u] - uv[u + 2]))
+                            + ((uv[u + 1] - uv[u + 3]) * (uv[u + 1] - uv[u + 3])));
+            final double d2 = Math.sqrt(
+                    ((uv[u] - uv[u + 4]) * (uv[u] - uv[u + 4]))
+                            + ((uv[u + 1] - uv[u + 5]) * (uv[u + 1] - uv[u + 5])));
+            final double d3 = Math.sqrt(
+                    ((uv[u + 2] - uv[u + 4]) * (uv[u + 2] - uv[u + 4]))
+                            + ((uv[u + 3] - uv[u + 5]) * (uv[u + 3] - uv[u + 5])));
+            uvPerimeter[t] = d1 + d2 + d3;
         }
 
         double minX = Double.MAX_VALUE, minY = Double.MAX_VALUE,
@@ -302,6 +329,16 @@ public final class TriangleMeshBlock extends AbstractShape {
             if (maxX < rMinX || minX >= rMaxX || maxY < rMinY || minY >= rMaxY)
                 continue;
 
+            // Mipmap metric's visible side, once per slot instead of per
+            // tile: edge12 + edge13 + edge23, the same expression the
+            // paint core ran per tile (getDistanceTo sequence).
+            final double dx01 = sx0 - sx1, dy01 = sy0 - sy1;
+            final double dx02 = sx0 - sx2, dy02 = sy0 - sy2;
+            final double dx12 = sx1 - sx2, dy12 = sy1 - sy2;
+            screenPerim[slot][t] = Math.sqrt(dx01 * dx01 + dy01 * dy01)
+                    + Math.sqrt(dx02 * dx02 + dy02 * dy02)
+                    + Math.sqrt(dx12 * dx12 + dy12 * dy12);
+
             aggregator.queueShapeForRendering(handles[t]);
         }
     }
@@ -333,7 +370,7 @@ public final class TriangleMeshBlock extends AbstractShape {
             clipTtd[slot] = grownTtd;
         }
         final int base = used;
-        clipTtd[slot][base / CLIP_ENTRY] = origTtd(t);
+        clipTtd[slot][base / CLIP_ENTRY] = uvPerimeter[t];
 
         final double scale = renderingContext.projectionScale;
         final double centerX = renderingContext.centerCoordinate.x;
@@ -436,23 +473,17 @@ public final class TriangleMeshBlock extends AbstractShape {
         return clipTtd[slot][clipOffset / CLIP_ENTRY];
     }
 
+    /** The triangle's UV perimeter, precomputed at build (see field). */
+    double uvPerimeter(final int tri) {
+        return uvPerimeter[tri];
+    }
+
     /**
-     * The triangle's UV perimeter, with the exact expression and
-     * accumulation order of {@code TexturedTriangle}'s
-     * computeTotalTextureDistance: d(0,1) + d(0,2) + d(1,2).
+     * The triangle's screen-edge perimeter for this slot, precomputed in
+     * the transform loop (only valid for triangles queued unclipped).
      */
-    double origTtd(final int tri) {
-        final int u = tri * 6;
-        final double d1 = Math.sqrt(
-                ((uv[u] - uv[u + 2]) * (uv[u] - uv[u + 2]))
-                        + ((uv[u + 1] - uv[u + 3]) * (uv[u + 1] - uv[u + 3])));
-        final double d2 = Math.sqrt(
-                ((uv[u] - uv[u + 4]) * (uv[u] - uv[u + 4]))
-                        + ((uv[u + 1] - uv[u + 5]) * (uv[u + 1] - uv[u + 5])));
-        final double d3 = Math.sqrt(
-                ((uv[u + 2] - uv[u + 4]) * (uv[u + 2] - uv[u + 4]))
-                        + ((uv[u + 3] - uv[u + 5]) * (uv[u + 3] - uv[u + 5])));
-        return d1 + d2 + d3;
+    double screenPerim(final int slot, final int tri) {
+        return screenPerim[slot][tri];
     }
 
     /**