primitive chunk table in world snapshot instead of colliding long-keyed hashmap, bounded cache for tinted/composited mob and banner textures, vanilla dump with nested model json, village benchmark and perf test scenes

This commit is contained in:
MineTec committed 2026-11-04 16:59:42 +01:00
1 parent b5baffebbd
commit 88bae41a03
10 files changed
+306 -38

No files matched your search

+49 -1
View File
@@ -24,7 +24,7 @@ import java.util.logging.Logger;
* 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.
* farm_items, village, normal. {@code -Djava.util.concurrent.ForkJoinPool.common.parallelism=N} pins the thread count.
*/
public class EntityBenchmark {
@@ -114,6 +114,33 @@ public class EntityBenchmark {
}
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<>();
@@ -142,11 +169,32 @@ public class EntityBenchmark {
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;
};
}
+5 -5
View File
@@ -1,4 +1,3 @@
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
@@ -31,7 +30,8 @@ import net.minecraft.world.entity.EntityType;
/**
* Dumps the vanilla Java entity models straight from an (unobfuscated, 26.x+) Minecraft CLIENT jar into
* the CEM-template JSON layout PixelPics loads ({@code {"models": {name: {"model": "<jem json>"}}}}).
* the CEM-template JSON layout PixelPics loads ({@code {"models": {name: {"model": {<jem>}}}}}; the jem is a
* nested object, not the template set's embedded JSON string, so the plugin parses the file only once).
*
* <p>Unlike the third-party CEM template set (rest geometry only), every entity model is instantiated
* through its real renderer and posed by its real {@code setupAnim} with a default (idle, adult, not
@@ -152,7 +152,7 @@ public class VanillaModelDump {
parts.add(part("root", rootDef, baked, 0, 0, 0));
jem.add("models", parts);
JsonObject entry = new JsonObject();
entry.addProperty("model", new Gson().toJson(jem));
entry.add("model", jem); // nested object, not a JSON string: parsed once at plugin start
out.putIfAbsent(name, entry);
} catch (Throwable t) {
failed.add(name + " (layer: " + t + ")");
@@ -315,7 +315,7 @@ public class VanillaModelDump {
JsonObject jem = dump(factory.apply(set.bakeLayer(loc)), state, name, failed);
if (jem == null) return;
JsonObject entry = new JsonObject();
entry.addProperty("model", new Gson().toJson(jem));
entry.add("model", jem);
out.put(name, entry);
} catch (Throwable t) {
failed.add(name + " (" + t + ")");
@@ -368,7 +368,7 @@ public class VanillaModelDump {
String tex = textures.get(f);
if (tex != null) jem.addProperty("texture", tex);
JsonObject entry = new JsonObject();
entry.addProperty("model", new Gson().toJson(jem));
entry.add("model", jem);
out.put(name, entry);
}
}
+69
View File
@@ -141,6 +141,10 @@ public class PPTest extends JavaPlugin {
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));
// Per-mob texture compositing (tinted-texture cache): villager type/profession/level, sheep colours, dyed
// leather armor, wolf collars and patterned banners, all from a seeded RNG (deterministic).
scenes.put("perf_village", new Scene("perf_village", this::perfVillage, -6, 9, -13, 4, 0, 0));
scenes.put("perf_village_close", new Scene("perf_village_close", this::perfVillage, 0.5, 3, -11.5, 5, 0.5, -4));
}
private void perfFarm(Location o) {
@@ -156,6 +160,71 @@ public class PPTest extends JavaPlugin {
}
}
private void perfVillage(Location o) {
Random r = new Random(11);
List<Villager.Type> vTypes = registry(io.papermc.paper.registry.RegistryKey.VILLAGER_TYPE);
List<Villager.Profession> profs = registry(io.papermc.paper.registry.RegistryKey.VILLAGER_PROFESSION);
DyeColor[] dyes = DyeColor.values();
for (int i = 0; i < 14; i++)
for (int j = 0; j < 16; j++) {
double x = -5 + i * 1.2, z = -10 + j * 1.2;
DyeColor dye = dyes[r.nextInt(dyes.length)];
LivingEntity e;
switch (r.nextInt(4)) {
case 0 -> {
Villager v = rfSpawn(o, x, 0, z, 0, Villager.class);
v.setVillagerType(vTypes.get(r.nextInt(vTypes.size())));
if (r.nextInt(6) == 0) v.setBaby();
else {
v.setProfession(profs.get(r.nextInt(profs.size())));
v.setVillagerLevel(1 + r.nextInt(5));
}
e = v;
}
case 1 -> {
Sheep sh = rfSpawn(o, x, 0, z, 0, Sheep.class);
sh.setColor(dye);
if (r.nextInt(6) == 0) sh.setBaby();
e = sh;
}
case 2 -> {
Zombie zo = rfSpawn(o, x, 0, z, 0, Zombie.class);
Material[] pieces = {Material.LEATHER_HELMET, Material.LEATHER_CHESTPLATE,
Material.LEATHER_LEGGINGS, Material.LEATHER_BOOTS};
ItemStack[] armor = new ItemStack[4];
for (int k = 0; k < 4; k++) {
armor[k] = it(pieces[k]);
org.bukkit.inventory.meta.LeatherArmorMeta m =
(org.bukkit.inventory.meta.LeatherArmorMeta) armor[k].getItemMeta();
m.setColor(dye.getColor());
armor[k].setItemMeta(m);
}
zo.getEquipment().setHelmet(armor[0]);
zo.getEquipment().setChestplate(armor[1]);
zo.getEquipment().setLeggings(armor[2]);
zo.getEquipment().setBoots(armor[3]);
e = zo;
}
default -> {
Wolf wo = rfSpawn(o, x, 0, z, 0, Wolf.class);
tame(wo, dye);
if (r.nextInt(6) == 0) wo.setBaby();
e = wo;
}
}
e.setBodyYaw(r.nextInt(8) * 45);
}
for (int b = 0; b < 10; b++) {
int x = -4 + b * 2;
put(o, x, 0, 10, dyes[r.nextInt(dyes.length)].name().toLowerCase() + "_banner[rotation=" + r.nextInt(16) + "]");
if (o.clone().add(x, 0, 10).getBlock().getState() instanceof org.bukkit.block.Banner bn) {
bn.addPattern(new org.bukkit.block.banner.Pattern(dyes[r.nextInt(dyes.length)], org.bukkit.block.banner.PatternType.STRIPE_TOP));
bn.addPattern(new org.bukkit.block.banner.Pattern(dyes[r.nextInt(dyes.length)], org.bukkit.block.banner.PatternType.CREEPER));
bn.update(true, false);
}
}
}
private void perfItems(Location o) {
Random r = new Random(7);
Material[] mats = {Material.WHEAT, Material.CARROT, Material.EGG, Material.BONE, Material.ROTTEN_FLESH,