entity bvh with exact old tie order and allocation-free entity/element intersectors, entity benchmark harness and perf test scenes

This commit is contained in:
MineTec committed 2026-11-04 16:42:08 +01:00
1 parent 2d56a1910c
commit b5baffebbd
6 files changed
+910 -161

No files matched your search

+195
View File
@@ -0,0 +1,195 @@
import eu.mhsl.minecraft.pixelpics.assets.*;
import eu.mhsl.minecraft.pixelpics.render.entity.*;
import eu.mhsl.minecraft.pixelpics.render.render.*;
import eu.mhsl.minecraft.pixelpics.render.sky.SkyContext;
import eu.mhsl.minecraft.pixelpics.render.snapshot.WorldSnapshot;
import eu.mhsl.minecraft.pixelpics.render.tint.BiomeTintProvider;
import org.bukkit.ChunkSnapshot;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.data.BlockData;
import org.bukkit.util.Vector;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.lang.reflect.Proxy;
import java.util.*;
import java.util.List;
import java.util.logging.Logger;
/**
* Reproducible render benchmark (no server): farm scenes with hundreds of mobs / dropped items on a block
* floor plus a "normal" scene (few mobs, translucent + additive layers). Renders every scene {@code -Druns}
* times (after {@code -Dwarmup} runs), prints the median wall time and writes one PNG per scene to
* {@code -Dout} (default ~/Dokumente/PixelPics-Entity-Renders/bench) for before/after pixel diffs.
* Args: scene names to run (default all): farm_mobs, farm_empty (same view, no entities), farm_mobs_close,
* farm_items, normal. {@code -Djava.util.concurrent.ForkJoinPool.common.parallelism=N} pins the thread count.
*/
public class EntityBenchmark {
static final String ROOT = "/home/elias/Dokumente/mcTestServer/plugins/PixelPics";
static final int W = 320, H = 240, SSAA = 3;
record Scene(String name, Vector cam, Vector look, List<EntityState> mobs, List<ObjectState> objects) {}
public static void main(String[] args) throws Exception {
Logger log = Logger.getLogger("bench");
ResourcePack pack = ResourcePackLoader.load(new File(ROOT, "resourcepack"), log).orElseThrow();
AssetReader reader = new AssetReader(pack);
TextureCache textures = new TextureCache(pack);
BlockModelRegistry registry = new BlockModelRegistry(reader, textures);
BiomeTintProvider tint = new BiomeTintProvider(textures);
var geo = new eu.mhsl.minecraft.pixelpics.render.entity.cem.CemModelLoader();
geo.load(new java.io.FileInputStream("src/main/resources/cem/cem_template_models.json"), log);
geo.load(new java.io.FileInputStream("src/main/resources/cem/vanilla_entity_models.json"), log);
var font = eu.mhsl.minecraft.pixelpics.assets.font.FontLoader.load(pack, textures, log);
SkinCache skins = new SkinCache();
var baker = new eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker(geo, textures, skins);
var beBaker = new eu.mhsl.minecraft.pixelpics.render.entity.cem.BlockEntityBaker(geo, textures, skins, font);
DefaultScreenRenderer renderer = new DefaultScreenRenderer(registry, tint, textures, baker, beBaker, log);
WorldSnapshot world = farmWorld();
SkyContext sky = new SkyContext(6000, 0, 6000);
int runs = Integer.getInteger("runs", 5), warmup = Integer.getInteger("warmup", 2);
File outDir = new File(System.getProperty("out", "/home/elias/Dokumente/PixelPics-Entity-Renders/bench"));
outDir.mkdirs();
Set<String> only = new HashSet<>(Arrays.asList(args));
for (Scene sc : scenes()) {
if (!only.isEmpty() && !only.contains(sc.name)) continue;
Location loc = new Location(null, sc.cam.getX(), sc.cam.getY(), sc.cam.getZ());
loc.setDirection(sc.look.clone().subtract(sc.cam));
List<Vector> rayMap = EntityTestRender.buildRayMap(loc, W * SSAA, H * SSAA);
RenderJob job = new RenderJob(world, rayMap, sc.cam, W, H, sky, sc.mobs, List.of(), List.of(), sc.objects);
BufferedImage img = null;
for (int i = 0; i < warmup; i++) img = renderer.execute(job);
long[] times = new long[runs];
for (int i = 0; i < runs; i++) {
long t0 = System.nanoTime();
img = renderer.execute(job);
times[i] = System.nanoTime() - t0;
}
Arrays.sort(times);
ImageIO.write(img, "png", new File(outDir, sc.name + ".png"));
System.out.printf("BENCH %-16s mobs=%4d objects=%4d median %7.1f ms min %7.1f ms%n", sc.name,
sc.mobs.size(), sc.objects.size(), times[runs / 2] / 1e6, times[0] / 1e6);
}
}
static List<Scene> scenes() {
List<Scene> out = new ArrayList<>();
// Mob farm: 400 animals in a 20x20 pen (1.4 block spacing), seen from an elevated corner.
List<EntityState> farm = new ArrayList<>();
Random r = new Random(42);
String[] types = {"cow", "sheep", "chicken", "pig"};
for (int i = 0; i < 20; i++) {
for (int j = 0; j < 20; j++) {
String type = types[r.nextInt(types.length)];
farm.add(mob(type, i * 1.4 + r.nextDouble() * 0.4, 1, j * 1.4 + r.nextDouble() * 0.4,
r.nextFloat() * 360, r.nextInt(5) == 0));
}
}
out.add(new Scene("farm_mobs", new Vector(-6, 14, -6), new Vector(14, 1, 14), farm, List.of()));
// Same view without entities: farm_mobs minus this = the entity share of the render time.
out.add(new Scene("farm_empty", new Vector(-6, 14, -6), new Vector(14, 1, 14), List.of(), List.of()));
out.add(new Scene("farm_mobs_close", new Vector(3, 4.5, -3), new Vector(10, 1, 8), farm, List.of()));
// Item farm: 500 dropped items (flat sprites + block items) piled on a 10x10 collection area.
List<ObjectState> items = new ArrayList<>();
String[] flat = {"wheat", "carrot", "egg", "bone", "rotten_flesh", "string", "gunpowder", "iron_ingot"};
Material[] blocks = {Material.COBBLESTONE, Material.OAK_LOG, Material.PUMPKIN};
String[] blockState = {null, "axis=y", null};
for (int i = 0; i < 500; i++) {
double x = 2 + r.nextDouble() * 10, z = 2 + r.nextDouble() * 10, y = 1.1 + r.nextDouble() * 0.3;
Affine a = Affine.translation(x, y, z).mul(Affine.rotY(r.nextDouble() * Math.PI * 2)).mul(Affine.scale(0.5));
ItemSpec spec;
if (r.nextInt(4) == 0) {
int b = r.nextInt(blocks.length);
spec = new ItemSpec(blocks[b].getKey().getKey(), ObjectTestRender.block(blocks[b], blockState[b]), null, false);
} else {
spec = new ItemSpec(flat[r.nextInt(flat.length)], null, null, false);
}
items.add(ObjectState.item(a, spec, "ground", true));
}
out.add(new Scene("farm_items", new Vector(0, 7, -1), new Vector(7, 1, 7), List.of(), items));
// Normal scene: a handful of mobs incl. translucent (slime) and additive (charged creeper) layers,
// a text display with the default translucent background and two items.
List<EntityState> few = new ArrayList<>();
few.add(mob("cow", 3, 1, 3, 30, false));
few.add(mob("sheep", 5, 1, 4, 200, false));
few.add(mob("villager", 4, 1, 6, 160, false));
few.add(mob("zombie", 6.5, 1, 6, 220, false));
few.add(mob("slime", 2, 1, 5.5, 120, false));
EntityState creeper = mob("creeper", 7, 1, 3.5, 250, false);
few.add(withPowered(creeper));
few.add(mob("chicken", 3.5, 1, 1.5, 90, false));
List<ObjectState> objs = new ArrayList<>();
objs.add(ObjectState.item(Affine.translation(4.5, 1.2, 2).mul(Affine.scale(0.5)), new ItemSpec("apple", null, null, false), "ground", true));
objs.add(ObjectState.text(Affine.translation(4.5, 3.2, 4).mul(Affine.rotY(Math.PI)).mul(Affine.scale(0.025)),
List.of("PixelPics bench"), 200, 0xFFFFFFFF, 0x40000000, true));
out.add(new Scene("normal", new Vector(0, 3.5, -2.5), new Vector(4.5, 1.5, 4), few, objs));
return out;
}
static EntityState mob(String type, double x, double y, double z, float yaw, boolean baby) {
int tint = type.equals("sheep") ? 0xFFF9FFFE : 0;
return new EntityState(type, x, y, z, yaw, baby, 0.8, 1.0,
false, null, false, variant(type), tint, 1.0,
type.equals("villager") ? "farmer" : null, type.equals("villager") ? 2 : 0,
null, false, false, false, null, null,
false, 0, 0, 0, false, null, null, null, 0, false, null);
}
/** The variant the game always supplies (cow/pig/chicken have no texture without one). */
static String variant(String type) {
return switch (type) {
case "villager" -> "plains";
case "cow", "pig", "chicken" -> "temperate";
default -> null;
};
}
static EntityState withPowered(EntityState s) {
return new EntityState(s.typeKey(), s.x(), s.y(), s.z(), s.bodyYaw(), s.baby(), s.width(), s.height(),
s.player(), s.skinUrl(), s.slim(), s.variant(), s.tint(), s.sizeScale(), s.profession(), s.villagerLevel(),
s.markings(), s.saddle(), s.chest(), s.arms(), s.bodyEquip(), s.equipment(), s.invisible(), s.pitch(),
s.bodyDye(), s.tint2(), s.passenger(), s.rightHand(), s.leftHand(), s.headItem(), s.collar(), true,
s.mouthItem());
}
/**
* Grass floor at y=0 over [-48, 48)². Nothing stacked on it: ambient occlusion asks Material.isOccluding()
* for non-air neighbours, which needs a server registry (not available standalone).
*/
static WorldSnapshot farmWorld() {
BlockData air = ObjectTestRender.block(Material.AIR, null);
BlockData grass = ObjectTestRender.block(Material.GRASS_BLOCK, "snowy=false");
Map<Long, ChunkSnapshot> chunks = new HashMap<>();
for (int cx = -3; cx < 3; cx++) {
for (int cz = -3; cz < 3; cz++) {
final int bx0 = cx * 16, bz0 = cz * 16;
ChunkSnapshot cs = (ChunkSnapshot) Proxy.newProxyInstance(EntityBenchmark.class.getClassLoader(),
new Class<?>[]{ChunkSnapshot.class}, (p, m, a) -> {
switch (m.getName()) {
case "getBlockData": {
int y = (int) a[1];
return y == 0 ? grass : air;
}
case "getBiome": return null;
case "equals": return p == a[0];
case "hashCode": return System.identityHashCode(p);
case "toString": return "chunk";
}
Class<?> rt = m.getReturnType();
if (rt == boolean.class) return false;
if (rt.isPrimitive()) return 0;
return null;
});
chunks.put(WorldSnapshot.chunkKey(cx, cz), cs);
}
}
return new WorldSnapshot(chunks, 0, 8, air);
}
}
+34
View File
@@ -136,6 +136,38 @@ public class PPTest extends JavaPlugin {
scenes.put("frames_side", new Scene("frames_side", this::frames, -1.5, 3.2, -1.0, 4.5, 2.6, 3.6));
scenes.put("frames_dirs_ne", new Scene("frames_dirs_ne", this::framesDirs, 16, 7.5, -4, 8, 4, 3.5));
scenes.put("frames_dirs_sw", new Scene("frames_dirs_sw", this::framesDirs, 0, 1.2, 10, 8, 4, 3.5));
// Performance (entity BVH): a 15x18 animal farm (270 mobs, randomizeData=false, fixed body yaw) and 300
// item displays from a seeded RNG -> deterministic, pixel-diffable; the render time is logged.
scenes.put("perf_farm", new Scene("perf_farm", this::perfFarm, -6, 9, -13, 4, 0, 0));
scenes.put("perf_farm_close", new Scene("perf_farm_close", this::perfFarm, 0.5, 3, -11.5, 5, 0.5, -4));
scenes.put("perf_items", new Scene("perf_items", this::perfItems, 0.5, 5, -10, 5, 0, -2));
}
private void perfFarm(Location o) {
Random r = new Random(42);
Class<?>[] types = {Cow.class, Sheep.class, Chicken.class, Pig.class};
for (int i = 0; i < 15; i++)
for (int j = 0; j < 18; j++) {
@SuppressWarnings("unchecked")
Class<? extends LivingEntity> type = (Class<? extends LivingEntity>) types[r.nextInt(types.length)];
LivingEntity e = rfSpawn(o, -5 + i * 1.2, 0, -10 + j * 1.2, 0, type);
if (r.nextInt(5) == 0 && e instanceof Ageable a) a.setBaby();
e.setBodyYaw(r.nextInt(8) * 45);
}
}
private void perfItems(Location o) {
Random r = new Random(7);
Material[] mats = {Material.WHEAT, Material.CARROT, Material.EGG, Material.BONE, Material.ROTTEN_FLESH,
Material.STRING, Material.GUNPOWDER, Material.IRON_INGOT, Material.COBBLESTONE, Material.OAK_LOG,
Material.PUMPKIN};
for (int i = 0; i < 300; i++) {
ItemDisplay d = spawn(o, -4 + r.nextDouble() * 16, 0.15 + r.nextDouble() * 0.2, -10 + r.nextDouble() * 16,
r.nextInt(360), ItemDisplay.class);
d.setItemStack(it(mats[r.nextInt(mats.length)]));
d.setTransformation(new Transformation(new Vector3f(), new AxisAngle4f((float) (Math.PI / 2), 1, 0, 0),
new Vector3f(0.4f), new AxisAngle4f()));
}
}
@Override
@@ -223,7 +255,9 @@ public class PPTest extends JavaPlugin {
+ " objects, " + job.blockEntities().size() + " block entities");
Bukkit.getScheduler().runTaskAsynchronously(this, () -> {
try {
long t0 = System.nanoTime();
BufferedImage img = r.execute(job);
getLogger().info(String.format("TIME %s %.1f ms", sc.name, (System.nanoTime() - t0) / 1e6));
File f = new File(getDataFolder(), sc.name + ".png");
f.getParentFile().mkdirs();
ImageIO.write(img, "png", f);