244 lines
14 KiB
Java
244 lines
14 KiB
Java
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, village, 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));
|
|
|
|
// Village: 300 mobs with per-mob texture compositing (villager type/profession/level overlays, dyed
|
|
// sheep wool, dyed leather armor, tinted wolf collars) — exercises the tinted-texture cache.
|
|
List<EntityState> village = new ArrayList<>();
|
|
String[] vTypes = {"plains", "desert", "jungle", "savanna", "snow", "swamp", "taiga"};
|
|
String[] profs = {"armorer", "butcher", "cartographer", "cleric", "farmer", "fisherman", "fletcher",
|
|
"leatherworker", "librarian", "mason", "shepherd", "toolsmith", "weaponsmith", "nitwit", "none"};
|
|
Random rv = new Random(7);
|
|
for (int i = 0; i < 15; i++) {
|
|
for (int j = 0; j < 20; j++) {
|
|
double x = i * 1.4 + rv.nextDouble() * 0.3, z = j * 1.4 + rv.nextDouble() * 0.3;
|
|
float yaw = rv.nextFloat() * 360;
|
|
int dye = DYES[rv.nextInt(DYES.length)];
|
|
EntityState m = switch (rv.nextInt(4)) {
|
|
case 0 -> villager(x, z, yaw, vTypes[rv.nextInt(vTypes.length)], profs[rv.nextInt(profs.length)],
|
|
1 + rv.nextInt(5), rv.nextInt(6) == 0);
|
|
case 1 -> with(mob("sheep", x, 1, z, yaw, rv.nextInt(6) == 0), dye, null, 0);
|
|
case 2 -> {
|
|
EntityState.EquipPiece le = new EntityState.EquipPiece("leather", dye, null, null, false);
|
|
yield with(mob("zombie", x, 1, z, yaw, false), 0, new EntityState.Equipment(le, le, le, le), 0);
|
|
}
|
|
default -> with(mob("wolf", x, 1, z, yaw, rv.nextInt(6) == 0), 0, null, dye);
|
|
};
|
|
village.add(m);
|
|
}
|
|
}
|
|
out.add(new Scene("village", new Vector(-5, 10, -5), new Vector(10, 1, 13), village, List.of()));
|
|
|
|
// 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);
|
|
}
|
|
|
|
/** Vanilla DyeColor texture colours (white … black). */
|
|
static final int[] DYES = {0xFFF9FFFE, 0xFFF9801D, 0xFFC74EBD, 0xFF3AB3DA, 0xFFFED83D, 0xFF80C71F, 0xFFF38BAA,
|
|
0xFF474F52, 0xFF9D9D97, 0xFF169C9C, 0xFF8932B8, 0xFF3C44AA, 0xFF835432, 0xFF5E7C16, 0xFFB02E26, 0xFF1D1D21};
|
|
|
|
static EntityState villager(double x, double z, float yaw, String type, String prof, int level, boolean baby) {
|
|
return new EntityState("villager", x, 1, z, yaw, baby, 0.6, 1.95,
|
|
false, null, false, type, 0, 1.0, prof, level,
|
|
null, false, false, false, null, null,
|
|
false, 0, 0, 0, false, null, null, null, 0, false, null);
|
|
}
|
|
|
|
/** {@code s} with wool tint, humanoid armor and collar dye replaced. */
|
|
static EntityState with(EntityState s, int tint, EntityState.Equipment eq, int collar) {
|
|
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(), tint, s.sizeScale(), s.profession(), s.villagerLevel(),
|
|
s.markings(), s.saddle(), s.chest(), s.arms(), s.bodyEquip(), eq, s.invisible(), s.pitch(),
|
|
s.bodyDye(), s.tint2(), s.passenger(), s.rightHand(), s.leftHand(), s.headItem(), collar, s.powered(),
|
|
s.mouthItem());
|
|
}
|
|
|
|
/** 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";
|
|
case "wolf" -> "pale";
|
|
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);
|
|
}
|
|
}
|