add RenderManager: improve CPU control with global render queue and parallel trace pooling

This commit is contained in:
MineTec committed 2026-06-21 17:49:57 +02:00
1 parent 220cda1deb
commit f83ccdc7ff
7 files changed
+312 -62

No files matched your search

@@ -52,8 +52,9 @@ public class CameraListener implements Listener {
return;
}
// Consume one film and update the in-hand camera (count + lore) before the shot.
player.getInventory().setItemInMainHand(CameraItems.withFilmCount(inHand, film - 1));
PhotoService.takePhoto(player);
// Only charge film if the shot was actually accepted (renderer ready, not queue-rejected).
if (PhotoService.takePhoto(player)) {
player.getInventory().setItemInMainHand(CameraItems.withFilmCount(inHand, film - 1));
}
}
}
@@ -1,6 +1,7 @@
package eu.mhsl.minecraft.pixelpics.survival;
import eu.mhsl.minecraft.pixelpics.Main;
import eu.mhsl.minecraft.pixelpics.render.RenderManager;
import eu.mhsl.minecraft.pixelpics.render.render.DefaultScreenRenderer;
import eu.mhsl.minecraft.pixelpics.render.render.RenderJob;
import eu.mhsl.minecraft.pixelpics.render.render.Resolution;
@@ -26,15 +27,22 @@ import java.util.UUID;
/**
* Renders a photo of a player's current view and delivers it as a developing {@code FILLED_MAP},
* with action-bar feedback, a shutter sound + flash particles on capture, and a chime when the
* picture finishes developing. Shared by the camera item (survival) and the {@code /pixelPic}
* debug command. Film accounting is the caller's responsibility — this method never touches film.
* with a shutter sound + flash particles on capture and a chime when the picture finishes developing.
* Heavy work is funneled through {@link RenderManager} (per-player single slot, global concurrency
* limit + queue, bounded low-priority CPU). Shared by the camera item (survival) and the
* {@code /pixelPic} debug command. Film accounting is the caller's responsibility.
*/
public final class PhotoService {
/** Render result carried back to the main thread: PNG source + dithered map indices. */
private record RenderOutput(BufferedImage image, byte[] indices) {}
private PhotoService() {}
/** Captures and delivers a photo. Returns {@code false} if the renderer is unavailable. */
/**
* Captures and delivers a photo. Returns {@code false} (with action-bar feedback) if the renderer
* is unavailable or the request is rejected by the queue — callers should not charge film then.
*/
public static boolean takePhoto(Player player) {
Main plugin = Main.getInstance();
DefaultScreenRenderer renderer = plugin.getScreenRenderer();
@@ -44,50 +52,60 @@ public final class PhotoService {
return false;
}
Resolution resolution = new Resolution(Resolution.Pixels._128P, Resolution.AspectRatio._1_1);
RenderManager manager = plugin.getRenderManager();
UUID user = player.getUniqueId();
RenderManager.Outcome outcome = manager.tryReserve(user);
if (outcome != RenderManager.Outcome.ACCEPTED) {
player.sendActionBar(Component.text(outcome == RenderManager.Outcome.USER_BUSY
? "Deine letzte Aufnahme wird noch entwickelt …"
: "Zu viele Aufnahmen gerade — versuch es gleich erneut.", NamedTextColor.RED));
return false;
}
// Capture the world snapshot on the main thread.
RenderJob job = renderer.prepare(player.getEyeLocation(), resolution, player.getUniqueId());
boolean dispatched = false;
try {
Resolution resolution = new Resolution(Resolution.Pixels._128P, Resolution.AspectRatio._1_1);
// Hand the map over immediately, showing blank "film"; it develops once the render is ready.
ImageMapRenderer mapRenderer = new ImageMapRenderer();
MapView mapView = Bukkit.createMap(player.getWorld());
int id = mapView.getId();
MapManager.attachView(mapView, mapRenderer);
// Capture the world snapshot on the main thread (the moment of the shot).
RenderJob job = renderer.prepare(player.getEyeLocation(), resolution, user);
ItemStack map = new ItemStack(Material.FILLED_MAP, 1);
MapMeta meta = (MapMeta) map.getItemMeta();
meta.getPersistentDataContainer().set(plugin.pictureIdFlag,
PersistentDataType.STRING, UUID.randomUUID().toString());
meta.setMapView(mapView);
map.setItemMeta(meta);
player.getInventory().addItem(map);
// Hand the map over immediately, showing blank "film"; it develops once the render is ready.
ImageMapRenderer mapRenderer = new ImageMapRenderer();
MapView mapView = Bukkit.createMap(player.getWorld());
int id = mapView.getId();
MapManager.attachView(mapView, mapRenderer);
// Feedback is sound + particles only; no action-bar text on a normal shot.
playShutter(player);
ItemStack map = new ItemStack(Material.FILLED_MAP, 1);
MapMeta meta = (MapMeta) map.getItemMeta();
meta.getPersistentDataContainer().set(plugin.pictureIdFlag,
PersistentDataType.STRING, UUID.randomUUID().toString());
meta.setMapView(mapView);
map.setItemMeta(meta);
player.getInventory().addItem(map);
// Trace + dither off-thread, then start the developing animation on the main thread.
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
BufferedImage image;
byte[] indices;
try {
image = renderer.execute(job);
indices = MapImageDither.dither(image);
} catch (Exception e) {
plugin.getLogger().warning("Render failed: " + e.getMessage());
Bukkit.getScheduler().runTask(plugin, () ->
player.sendActionBar(Component.text("Rendern fehlgeschlagen.", NamedTextColor.RED)));
return;
}
BufferedImage finalImage = image;
byte[] finalIndices = indices;
Bukkit.getScheduler().runTask(plugin, () -> {
MapManager.saveImage(finalImage, id);
MapManager.saveIndices(finalIndices, id);
mapRenderer.develop(finalIndices);
player.playSound(player.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.8f, 1.2f);
});
});
// Feedback is sound + particles only; no action-bar text on a normal shot.
playShutter(player);
// Trace + dither off-thread (queued), then develop on the main thread. The cancel token is
// tripped by the watchdog on timeout; we bail out before the (now pointless) dithering.
manager.dispatch(user,
cancelled -> {
BufferedImage image = renderer.execute(job, cancelled);
if (cancelled.get()) return null;
return new RenderOutput(image, MapImageDither.dither(image));
},
out -> {
MapManager.saveImage(out.image(), id);
MapManager.saveIndices(out.indices(), id);
mapRenderer.develop(out.indices());
player.playSound(player.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.8f, 1.2f);
},
() -> player.sendActionBar(Component.text("Rendern fehlgeschlagen.", NamedTextColor.RED)));
dispatched = true;
} finally {
// prepare()/map setup threw before handing off — free the reserved slot.
if (!dispatched) manager.release(user);
}
return true;
}