add RenderManager: improve CPU control with global render queue and parallel trace pooling
This commit is contained in:
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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user