diff --git a/src/main/java/li/cil/oc2/client/renderer/ProjectorDepthRenderer.java b/src/main/java/li/cil/oc2/client/renderer/ProjectorDepthRenderer.java index 2a2134f1..470d9e89 100644 --- a/src/main/java/li/cil/oc2/client/renderer/ProjectorDepthRenderer.java +++ b/src/main/java/li/cil/oc2/client/renderer/ProjectorDepthRenderer.java @@ -231,6 +231,8 @@ public final class ProjectorDepthRenderer { renderProjectorDepthBuffer(minecraft, partialTicks, startNanos, viewModelStack); storeProjectorColorBuffer(projectorIndex, projector); + + projector.onRendering(); } } finally { finishDepthBufferRendering(minecraft, player); diff --git a/src/main/java/li/cil/oc2/common/Config.java b/src/main/java/li/cil/oc2/common/Config.java index e30ea3ad..5d9dde80 100644 --- a/src/main/java/li/cil/oc2/common/Config.java +++ b/src/main/java/li/cil/oc2/common/Config.java @@ -39,7 +39,7 @@ public final class Config { @Path("gameplay") public static ResourceLocation blockOperationsModuleToolTier = TierSortingRegistry.getName(Tiers.DIAMOND); @Path("admin") public static UUID fakePlayerUUID = UUID.fromString("e39dd9a7-514f-4a2d-aa5e-b6030621416d"); - @Path("admin.blocks") public static int projectorMaxBytesPerTick = 8192; + @Path("admin.network") public static int projectorAverageMaxBytesPerSecond = 160 * 1024; public static boolean robotsUseEnergy() { return robotEnergyPerTick > 0 && robotEnergyStorage > 0; diff --git a/src/main/java/li/cil/oc2/common/blockentity/ProjectorBlockEntity.java b/src/main/java/li/cil/oc2/common/blockentity/ProjectorBlockEntity.java index 7d8bc759..12558713 100644 --- a/src/main/java/li/cil/oc2/common/blockentity/ProjectorBlockEntity.java +++ b/src/main/java/li/cil/oc2/common/blockentity/ProjectorBlockEntity.java @@ -6,7 +6,8 @@ import li.cil.oc2.common.bus.device.vm.ProjectorVMDevice; import li.cil.oc2.common.capabilities.Capabilities; import li.cil.oc2.common.energy.FixedEnergyStorage; import li.cil.oc2.common.network.Network; -import li.cil.oc2.common.network.message.ProjectorFramebufferMessage; +import li.cil.oc2.common.network.ProjectorLoadBalancer; +import li.cil.oc2.common.network.message.ProjectorRequestFramebufferMessage; import li.cil.oc2.common.network.message.ProjectorStateMessage; import li.cil.oc2.jcodec.codecs.h264.H264Decoder; import li.cil.oc2.jcodec.codecs.h264.H264Encoder; @@ -18,7 +19,6 @@ import net.minecraft.core.Direction; import net.minecraft.core.SectionPos; import net.minecraft.nbt.CompoundTag; import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.phys.AABB; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -27,17 +27,14 @@ import javax.annotation.Nullable; import java.nio.BufferOverflowException; import java.nio.ByteBuffer; import java.util.Arrays; -import java.util.concurrent.ExecutionException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.atomic.AtomicInteger; import java.util.zip.DataFormatException; import java.util.zip.Deflater; import java.util.zip.Inflater; -// TODO Only send frames to watching clients (have clients send "keepalive" packets when rendering this). -// TODO Throttle update speed by distance to closest player and max number any player watching this projector is watching in total. public final class ProjectorBlockEntity extends ModBlockEntity implements TickableBlockEntity { @FunctionalInterface public interface FrameConsumer { @@ -53,15 +50,13 @@ public final class ProjectorBlockEntity extends ModBlockEntity implements Tickab public static final int MAX_WIDTH = MAX_GOOD_RENDER_DISTANCE + 1; // +1 To make it odd, so we can center. public static final int MAX_HEIGHT = (MAX_GOOD_RENDER_DISTANCE * ProjectorVMDevice.HEIGHT / ProjectorVMDevice.WIDTH) + 1; // + 1 To match horizontal margin. - private static final int FRAME_EVERY_N_TICKS = 5; - private static final String ENERGY_TAG_NAME = "energy"; private static final String IS_PROJECTING_TAG_NAME = "projecting"; - private static final ExecutorService FRAME_WORKERS = Executors.newCachedThreadPool(r -> { + private static final ExecutorService DECODER_WORKERS = Executors.newCachedThreadPool(r -> { final Thread thread = new Thread(r); thread.setDaemon(true); - thread.setName("Projector Frame Encoder/Decoder"); + thread.setName("Projector Frame Decoder"); return thread; }); @@ -70,24 +65,21 @@ public final class ProjectorBlockEntity extends ModBlockEntity implements Tickab private final ProjectorVMDevice projectorDevice = new ProjectorVMDevice(this); private boolean isProjecting, hasEnergy; private final FixedEnergyStorage energy = new FixedEnergyStorage(Config.projectorEnergyStorage); - - // Video transfer. - private final H264Encoder encoder = new H264Encoder(new CQPRateControl(12)); - @Nullable private Future runningEncode; // To allow waiting for previous frame to finish. - private final ByteBuffer encoderBuffer = ByteBuffer.allocateDirect(1024 * 1024); // Re-used decompression buffer. private final Picture picture = Picture.create(ProjectorVMDevice.WIDTH, ProjectorVMDevice.HEIGHT, ColorSpace.YUV420J); + // Video encoding. + private final H264Encoder encoder = new H264Encoder(new CQPRateControl(12)); + private final ByteBuffer encoderBuffer = ByteBuffer.allocateDirect(1024 * 1024); // Re-used decompression buffer. private boolean needsIDR = true; // Whether we need to send a keyframe next. - private final AtomicInteger sendBudget = new AtomicInteger(); // Remaining accumulated bandwidth budget. - private int nextFrameIn = 0; // Remaining cooldown before sending next frame. - // Client only data. + // Video decoding. private final H264Decoder decoder = new H264Decoder(); - @Nullable private Future runningDecode; // Current decoding operation, if any, to avoid race conditions. + @Nullable private CompletableFuture runningDecode; // Current decoding operation, if any, to avoid race conditions. private final ByteBuffer decoderBuffer = ByteBuffer.allocateDirect(1024 * 1024); // Re-used decompression buffer. @Nullable private FrameConsumer frameConsumer; // Where to throw received frames. - private AABB renderBounds; // Overall render bounds, disregarding projection surface, to allow growing if necessary. + private AABB renderBounds; // Maximum possible render bounds, assuming we project on furthest away surface. + private long lastKeepAliveSentAt; /////////////////////////////////////////////////////////////// @@ -132,7 +124,11 @@ public final class ProjectorBlockEntity extends ModBlockEntity implements Tickab needsIDR = true; } - sendRunningState(); + sendProjectorState(); + } + + public void setRequiresKeyframe() { + needsIDR = true; } public void setFrameConsumer(@Nullable final FrameConsumer consumer) { @@ -147,6 +143,14 @@ public final class ProjectorBlockEntity extends ModBlockEntity implements Tickab } } + public void onRendering() { + final long now = System.currentTimeMillis(); + if (now - lastKeepAliveSentAt > 1000) { + lastKeepAliveSentAt = now; + Network.sendToServer(new ProjectorRequestFramebufferMessage(this)); + } + } + @Override public void serverTick() { if (!isProjecting()) { @@ -156,63 +160,19 @@ public final class ProjectorBlockEntity extends ModBlockEntity implements Tickab if (energy.extractEnergy(Config.projectorEnergyPerTick, true) < Config.projectorEnergyPerTick) { if (hasEnergy) { hasEnergy = false; - sendRunningState(); + sendProjectorState(); } return; } else if (!hasEnergy) { hasEnergy = true; - sendRunningState(); + sendProjectorState(); } - sendBudget.updateAndGet(budget -> Math.min(Config.projectorMaxBytesPerTick * 10, budget + Config.projectorMaxBytesPerTick)); - nextFrameIn = Math.max(0, nextFrameIn - 1); - if (sendBudget.get() < 0 || nextFrameIn > 0) { + if (!projectorDevice.hasChanges()) { return; } - if (runningEncode != null && !runningEncode.isDone()) { - return; - } - - joinWorkerAndLogErrors(runningEncode); - - nextFrameIn = FRAME_EVERY_N_TICKS; - - if (level == null || !(level.getChunk(getBlockPos()) instanceof LevelChunk chunk)) { - return; - } - - runningEncode = FRAME_WORKERS.submit(() -> { - final boolean hasChanges = projectorDevice.applyChanges(picture); - if (!hasChanges && !needsIDR) { - return; - } - - encoderBuffer.clear(); - final ByteBuffer frameData; - try { - if (needsIDR) { - frameData = encoder.encodeIDRFrame(picture, encoderBuffer); - needsIDR = false; - } else { - frameData = encoder.encodeFrame(picture, encoderBuffer).data(); - } - } catch (final BufferOverflowException ignored) { - return; // Sad, frame encode failed for unknown reasons... - } - - final Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION); - deflater.setInput(frameData); - deflater.finish(); - final ByteBuffer compressedFrameData = ByteBuffer.allocateDirect(1024 * 1024); - deflater.deflate(compressedFrameData, Deflater.FULL_FLUSH); - deflater.end(); - compressedFrameData.flip(); - - sendBudget.accumulateAndGet(compressedFrameData.limit(), (budget, packetSize) -> budget - packetSize); - final ProjectorFramebufferMessage message = new ProjectorFramebufferMessage(this, compressedFrameData); - Network.sendToClientsTrackingChunk(message, chunk); - }); + ProjectorLoadBalancer.offerFrame(this, this::encodeFrame); } @Override @@ -260,10 +220,13 @@ public final class ProjectorBlockEntity extends ModBlockEntity implements Tickab } public void applyNextFrame(final ByteBuffer frameData) { - final Future lastDecode = runningDecode; - runningDecode = FRAME_WORKERS.submit(() -> { + final CompletableFuture lastDecode = runningDecode; + runningDecode = CompletableFuture.runAsync(() -> { try { - joinWorkerAndLogErrors(lastDecode); + try { + if (lastDecode != null) lastDecode.join(); + } catch (final CompletionException ignored) { + } final Inflater inflater = new Inflater(); inflater.setInput(frameData); @@ -281,7 +244,7 @@ public final class ProjectorBlockEntity extends ModBlockEntity implements Tickab } } catch (final DataFormatException ignored) { } - }); + }, DECODER_WORKERS); } /////////////////////////////////////////////////////////////// @@ -299,12 +262,43 @@ public final class ProjectorBlockEntity extends ModBlockEntity implements Tickab return Config.projectorEnergyStorage > 0 && Config.projectorEnergyPerTick > 0; } - private void sendRunningState() { + private void sendProjectorState() { if (level != null && !level.isClientSide()) { Network.sendToClientsTrackingBlockEntity(new ProjectorStateMessage(this, isProjecting && hasEnergy), this); } } + @Nullable + private ByteBuffer encodeFrame() { + final boolean hasChanges = projectorDevice.applyChanges(picture); + if (!hasChanges && !needsIDR) { + return null; + } + + encoderBuffer.clear(); + final ByteBuffer frameData; + try { + if (needsIDR) { + frameData = encoder.encodeIDRFrame(picture, encoderBuffer); + needsIDR = false; + } else { + frameData = encoder.encodeFrame(picture, encoderBuffer).data(); + } + } catch (final BufferOverflowException ignored) { + return null; + } + + final Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION); + deflater.setInput(frameData); + deflater.finish(); + final ByteBuffer compressedFrameData = ByteBuffer.allocateDirect(1024 * 1024); + deflater.deflate(compressedFrameData, Deflater.FULL_FLUSH); + deflater.end(); + compressedFrameData.flip(); + + return compressedFrameData; + } + private void updateRenderBounds() { final Direction blockFacing = getBlockState().getValue(ProjectorBlock.FACING); final Direction canvasUp = Direction.UP; @@ -319,17 +313,4 @@ public final class ProjectorBlockEntity extends ModBlockEntity implements Tickab renderBounds = new AABB(getBlockPos()).minmax(new AABB(screenMinPos)).minmax(new AABB(screenMaxPos)); } - - private static void joinWorkerAndLogErrors(@Nullable final Future job) { - if (job == null) { - return; - } - - try { - job.get(); - } catch (final InterruptedException ignored) { - } catch (final ExecutionException e) { - LOGGER.error(e); - } - } } diff --git a/src/main/java/li/cil/oc2/common/bus/device/vm/ProjectorVMDevice.java b/src/main/java/li/cil/oc2/common/bus/device/vm/ProjectorVMDevice.java index fdcd1abd..ed83c74b 100644 --- a/src/main/java/li/cil/oc2/common/bus/device/vm/ProjectorVMDevice.java +++ b/src/main/java/li/cil/oc2/common/bus/device/vm/ProjectorVMDevice.java @@ -44,13 +44,15 @@ public final class ProjectorVMDevice extends IdentityProxy /////////////////////////////////////////////////////////////// + public boolean hasChanges() { + synchronized (deviceLock) { + return device != null && device.hasChanges(); + } + } + public boolean applyChanges(final Picture picture) { synchronized (deviceLock) { - if (device != null) { - return device.applyChanges(picture); - } else { - return false; - } + return device != null && device.applyChanges(picture); } } diff --git a/src/main/java/li/cil/oc2/common/network/MessageUtils.java b/src/main/java/li/cil/oc2/common/network/MessageUtils.java index bc74f6b5..f004cbd2 100644 --- a/src/main/java/li/cil/oc2/common/network/MessageUtils.java +++ b/src/main/java/li/cil/oc2/common/network/MessageUtils.java @@ -10,20 +10,30 @@ import net.minecraft.world.entity.Entity; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraftforge.network.NetworkEvent; +import java.util.function.BiConsumer; import java.util.function.Consumer; public final class MessageUtils { - @SuppressWarnings("unchecked") - public static void withNearbyServerBlockEntityAt(final NetworkEvent.Context context, final BlockPos pos, final Class type, final Consumer callback) { + public static void withNearbyServerBlockEntityForInteraction(final NetworkEvent.Context context, final BlockPos pos, final Class type, final BiConsumer callback) { final ServerPlayer player = context.getSender(); if (player == null || !pos.closerThan(player.position(), 8)) { return; } + withNearbyServerBlockEntity(context, pos, type, callback); + } + + @SuppressWarnings("unchecked") + public static void withNearbyServerBlockEntity(final NetworkEvent.Context context, final BlockPos pos, final Class type, final BiConsumer callback) { + final ServerPlayer player = context.getSender(); + if (player == null) { + return; + } + final ServerLevel level = player.getLevel(); final BlockEntity blockEntity = LevelUtils.getBlockEntityIfChunkExists(level, pos); if (type.isInstance(blockEntity)) { - callback.accept((T) blockEntity); + callback.accept(player, (T) blockEntity); } } diff --git a/src/main/java/li/cil/oc2/common/network/Network.java b/src/main/java/li/cil/oc2/common/network/Network.java index cbdf0449..2458e0f5 100644 --- a/src/main/java/li/cil/oc2/common/network/Network.java +++ b/src/main/java/li/cil/oc2/common/network/Network.java @@ -74,6 +74,7 @@ public final class Network { registerMessage(NetworkInterfaceCardConfigurationMessage.class, NetworkInterfaceCardConfigurationMessage::new, NetworkDirection.PLAY_TO_SERVER); registerMessage(NetworkTunnelLinkMessage.class, NetworkTunnelLinkMessage::new, NetworkDirection.PLAY_TO_SERVER); + registerMessage(ProjectorRequestFramebufferMessage.class, ProjectorRequestFramebufferMessage::new, NetworkDirection.PLAY_TO_SERVER); registerMessage(ProjectorFramebufferMessage.class, ProjectorFramebufferMessage::new, NetworkDirection.PLAY_TO_CLIENT); registerMessage(ProjectorStateMessage.class, ProjectorStateMessage::new, NetworkDirection.PLAY_TO_CLIENT); } diff --git a/src/main/java/li/cil/oc2/common/network/ProjectorLoadBalancer.java b/src/main/java/li/cil/oc2/common/network/ProjectorLoadBalancer.java new file mode 100644 index 00000000..17af2e1e --- /dev/null +++ b/src/main/java/li/cil/oc2/common/network/ProjectorLoadBalancer.java @@ -0,0 +1,307 @@ +package li.cil.oc2.common.network; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.RemovalNotification; +import li.cil.oc2.api.API; +import li.cil.oc2.common.Config; +import li.cil.oc2.common.blockentity.ProjectorBlockEntity; +import li.cil.oc2.common.network.message.ProjectorFramebufferMessage; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.phys.Vec3; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.event.server.ServerStoppedEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +import javax.annotation.Nullable; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.WeakHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; + +import static java.util.Objects.requireNonNull; + +/** + * Mostly round-robin load balancer for allowing projectors to send data to clients. + *

+ * We try to satisfy two limits here: + *

    + *
  • + * Server side, limiting overall bandwidth consumed by projectors. + *
  • + *
  • + * Client side, limiting per-client bandwidth consumed by projectors. + *
  • + *
+ *

+ * To achieve this, there's a global budget and a per-projector skip count. The global budget + * controls overall data sent from the server. The skip counts modulate the round-robin behaviour + * of the load balancer. For example, projectors further away from their closest player will get + * a penalty, as will projectors with a large number of players watching them. + */ +@Mod.EventBusSubscriber(modid = API.MOD_ID, bus = Mod.EventBusSubscriber.Bus.FORGE) +public final class ProjectorLoadBalancer { + private static final long CACHE_EXPIRES_AFTER = 2000; /* In milliseconds */ + + /** + * Maps projectors to their specific info, i.e. players watching them and sending state. + *

+ * This is used to let projectors register new frames to be sent, and to register new players + * with already being tracked projectors. + *

+ * Player watching state is expired manually, by checking the last update time for players in + * the info instance every tick. This is required in case two players start watching a projector + * but then only one keeps watching it. In that case, we want to still remove the other player + * from the projector info, but keep the info for the still watching player. + */ + private static final Cache PROJECTOR_INFO = CacheBuilder.newBuilder() + .weakKeys() + .expireAfterWrite(Duration.ofMillis(CACHE_EXPIRES_AFTER)) + .removalListener(ProjectorLoadBalancer::handleProjectorInfoRemoved) + .build(); + + /** + * Global byte budget for sending stuff to clients. This is filled up every tick and consumed + * when sending packets to clients. We only send stuff when budget is non-negative. + */ + private static final AtomicInteger BUDGET = new AtomicInteger(getMaxBudget()); + + /** + * Pointer into our circular projector linked list pointing to the projector we last sent a packet for. + *

+ * At the same time represents the head of our doubly-linked, circular list of projectors. This list is + * used to do the round-robin load balancing, by advancing it to the next projector until we find one + * that can send something every tick. + */ + @Nullable private static ProjectorInfo lastSender; + + /** + * Updates timestamp of a player currently watching a projector. + */ + public static void updateWatcher(final ProjectorBlockEntity projector, final ServerPlayer player) { + try { + PROJECTOR_INFO.get(projector, () -> addProjectorInfo(projector)) + .players.put(player, System.currentTimeMillis()); + } catch (final ExecutionException e) { + throw new RuntimeException(e); + } + } + + /** + * Notifies the load balancer that a projector has data to send. + *

+ * Ignored if there are no players watching the projector. + */ + public static void offerFrame(final ProjectorBlockEntity projector, final Supplier messageSupplier) { + final ProjectorInfo info = PROJECTOR_INFO.getIfPresent(projector); + if (info != null) { + info.nextFrameSupplier = messageSupplier; + } + } + + /** + * Expires cached values. Checks if we can send something, and if so starts async + * generation of the package to send. + */ + @SubscribeEvent + public static void handleServerTick(final TickEvent.ServerTickEvent event) { + PROJECTOR_INFO.cleanUp(); + removeExpiredPlayers(); + + if (BUDGET.updateAndGet(ProjectorLoadBalancer::replenishBudget) > 0) { + sendNextReadyPacket(); + } + } + + /** + * Cleanup when server stops. + */ + @SubscribeEvent + public static void handleServerStopped(final ServerStoppedEvent event) { + PROJECTOR_INFO.invalidateAll(); + } + + private static int getMaxBudget() { + // We allow over-budgeting projectors to some degree, to allow short bursts of larger frame changes. + // Otherwise, this would be divided by twenty, since we attempt to send every tick. + return Config.projectorAverageMaxBytesPerSecond / 2; + } + + private static int replenishBudget(final int budget) { + return Math.min(getMaxBudget(), budget + Math.max(1, Config.projectorAverageMaxBytesPerSecond / 20)); + } + + private static ProjectorInfo addProjectorInfo(final ProjectorBlockEntity projector) { + projector.setRequiresKeyframe(); // When first watcher starts, immediately request keyframe. + final ProjectorInfo info = new ProjectorInfo(projector.getBlockPos()); + if (lastSender == null) { + // No sender yet, start the circle. + lastSender = info; + } else { + // Just add after last sender. + lastSender.add(info); + } + return info; + } + + private static void handleProjectorInfoRemoved(final RemovalNotification notification) { + final ProjectorInfo info = requireNonNull(notification.getValue()); + + if (lastSender == info) { + if (lastSender.next == lastSender) { + lastSender = null; // Last element in list, clear list. + } else { + lastSender = info.next; // Shift current entry to next. + } + } + + info.remove(); + } + + private static void removeExpiredPlayers() { + if (lastSender == null) { + return; + } + + final ProjectorInfo start = lastSender; + do { + lastSender.removeExpiredPlayers(); + lastSender = lastSender.next; + } while (lastSender != start); + } + + private static void sendNextReadyPacket() { + if (lastSender == null) { + return; + } + + final ProjectorInfo start = lastSender; + do { + lastSender = lastSender.next; + if (lastSender.sendIfReady()) { + return; + } + } while (lastSender != start); + } + + /** + * Tracks info for a single projector. This class is an entry in a circular double linked list, + * i.e. the last entry will always point back to the first entry, which makes looping over it + * for the round-robin load-balancing more comfortable. + */ + private static class ProjectorInfo { + private static final ExecutorService ENCODER_WORKERS = Executors.newCachedThreadPool(r -> { + final Thread thread = new Thread(r); + thread.setDaemon(true); + thread.setName("Projector Frame Encoder"); + return thread; + }); + + /** + * Pointers to next and previous entries in linked list. May point to this if it's the only entry. + */ + private ProjectorInfo next, previous; + + /** + * Position of the projector this info is for in the world. + */ + private final BlockPos projectorPos; + + /** + * The players watching this projector, to know where to send the data and how strongly to penalize. + */ + private final WeakHashMap players = new WeakHashMap<>(); + + /** + * The current penalty, in the form of rounds in the round-robin to skip. + */ + private int skipCount; + @Nullable private Supplier nextFrameSupplier; + @Nullable private Future runningEncode; + + public ProjectorInfo(final BlockPos projectorPos) { + next = previous = this; + this.projectorPos = projectorPos; + } + + public void add(final ProjectorInfo info) { + info.next = next; + next.previous = info; + next = info; + info.previous = this; + } + + public void remove() { + if (previous == null) { + return; + } + + previous.next = next; + next.previous = previous; + + previous = null; + next = null; + } + + public void removeExpiredPlayers() { + players.entrySet().removeIf(entry -> System.currentTimeMillis() - entry.getValue() > CACHE_EXPIRES_AFTER); + } + + public boolean sendIfReady() { + if (skipCount > 0) { + skipCount--; + return false; + } + + final boolean isReady = !players.isEmpty() && nextFrameSupplier != null && (runningEncode == null || runningEncode.isDone()); + if (isReady) { + sendAsync(); + updateSkipCount(); + } + + return isReady; + } + + private void sendAsync() { + assert nextFrameSupplier != null; + final Supplier frameSupplier = nextFrameSupplier; + nextFrameSupplier = null; + + assert runningEncode == null || runningEncode.isDone(); + runningEncode = ENCODER_WORKERS.submit(() -> { + final ByteBuffer frame = frameSupplier.get(); + final int budgetCost = frame.limit() * players.size(); + BUDGET.accumulateAndGet(budgetCost, (budget, cost) -> budget - cost); + + final ProjectorFramebufferMessage message = new ProjectorFramebufferMessage(projectorPos, frame); + for (final ServerPlayer player : players.keySet()) { + Network.sendToClient(message, player); + } + }); + } + + private void updateSkipCount() { + skipCount = 0; + + double closestPlayerDistanceSqr = Double.MAX_VALUE; + final Vec3 blockCenter = Vec3.atCenterOf(projectorPos); + for (final ServerPlayer player : players.keySet()) { + skipCount++; + final double distance = player.distanceToSqr(blockCenter); + closestPlayerDistanceSqr = Math.min(closestPlayerDistanceSqr, distance); + } + + final double closestPlayerDistance = Math.sqrt(closestPlayerDistanceSqr); + if (closestPlayerDistance > 16) { + skipCount++; + } + } + } +} diff --git a/src/main/java/li/cil/oc2/common/network/message/BusInterfaceNameMessage.java b/src/main/java/li/cil/oc2/common/network/message/BusInterfaceNameMessage.java index b52d1cb6..f118c7d7 100644 --- a/src/main/java/li/cil/oc2/common/network/message/BusInterfaceNameMessage.java +++ b/src/main/java/li/cil/oc2/common/network/message/BusInterfaceNameMessage.java @@ -69,8 +69,8 @@ public abstract class BusInterfaceNameMessage extends AbstractMessage { @Override protected void handleMessage(final NetworkEvent.Context context) { - MessageUtils.withNearbyServerBlockEntityAt(context, pos, BusCableBlockEntity.class, - busCable -> busCable.setInterfaceName(side, value)); + MessageUtils.withNearbyServerBlockEntityForInteraction(context, pos, BusCableBlockEntity.class, + (player, busCable) -> busCable.setInterfaceName(side, value)); } } } diff --git a/src/main/java/li/cil/oc2/common/network/message/ComputerPowerMessage.java b/src/main/java/li/cil/oc2/common/network/message/ComputerPowerMessage.java index ebcac2c2..5ab51b62 100644 --- a/src/main/java/li/cil/oc2/common/network/message/ComputerPowerMessage.java +++ b/src/main/java/li/cil/oc2/common/network/message/ComputerPowerMessage.java @@ -39,8 +39,8 @@ public final class ComputerPowerMessage extends AbstractMessage { @Override protected void handleMessage(final NetworkEvent.Context context) { - MessageUtils.withNearbyServerBlockEntityAt(context, pos, ComputerBlockEntity.class, - computer -> { + MessageUtils.withNearbyServerBlockEntityForInteraction(context, pos, ComputerBlockEntity.class, + (player, computer) -> { if (power) { computer.start(); } else { diff --git a/src/main/java/li/cil/oc2/common/network/message/ComputerTerminalInputMessage.java b/src/main/java/li/cil/oc2/common/network/message/ComputerTerminalInputMessage.java index df138a40..18735720 100644 --- a/src/main/java/li/cil/oc2/common/network/message/ComputerTerminalInputMessage.java +++ b/src/main/java/li/cil/oc2/common/network/message/ComputerTerminalInputMessage.java @@ -20,7 +20,7 @@ public final class ComputerTerminalInputMessage extends AbstractTerminalBlockMes @Override protected void handleMessage(final NetworkEvent.Context context) { - MessageUtils.withNearbyServerBlockEntityAt(context, pos, ComputerBlockEntity.class, - computer -> computer.getTerminal().putInput(ByteBuffer.wrap(data))); + MessageUtils.withNearbyServerBlockEntityForInteraction(context, pos, ComputerBlockEntity.class, + (player, computer) -> computer.getTerminal().putInput(ByteBuffer.wrap(data))); } } diff --git a/src/main/java/li/cil/oc2/common/network/message/OpenComputerInventoryMessage.java b/src/main/java/li/cil/oc2/common/network/message/OpenComputerInventoryMessage.java index 7eaccb53..543be6ce 100644 --- a/src/main/java/li/cil/oc2/common/network/message/OpenComputerInventoryMessage.java +++ b/src/main/java/li/cil/oc2/common/network/message/OpenComputerInventoryMessage.java @@ -4,7 +4,6 @@ import li.cil.oc2.common.blockentity.ComputerBlockEntity; import li.cil.oc2.common.network.MessageUtils; import net.minecraft.core.BlockPos; import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.server.level.ServerPlayer; import net.minecraftforge.network.NetworkEvent; public final class OpenComputerInventoryMessage extends AbstractMessage { @@ -35,10 +34,7 @@ public final class OpenComputerInventoryMessage extends AbstractMessage { @Override protected void handleMessage(final NetworkEvent.Context context) { - final ServerPlayer player = context.getSender(); - if (player != null) { - MessageUtils.withNearbyServerBlockEntityAt(context, pos, ComputerBlockEntity.class, - computer -> computer.openInventoryScreen(player)); - } + MessageUtils.withNearbyServerBlockEntityForInteraction(context, pos, ComputerBlockEntity.class, + (player, computer) -> computer.openInventoryScreen(player)); } } diff --git a/src/main/java/li/cil/oc2/common/network/message/OpenComputerTerminalMessage.java b/src/main/java/li/cil/oc2/common/network/message/OpenComputerTerminalMessage.java index 023ee8cc..55b68b5b 100644 --- a/src/main/java/li/cil/oc2/common/network/message/OpenComputerTerminalMessage.java +++ b/src/main/java/li/cil/oc2/common/network/message/OpenComputerTerminalMessage.java @@ -4,7 +4,6 @@ import li.cil.oc2.common.blockentity.ComputerBlockEntity; import li.cil.oc2.common.network.MessageUtils; import net.minecraft.core.BlockPos; import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.server.level.ServerPlayer; import net.minecraftforge.network.NetworkEvent; public final class OpenComputerTerminalMessage extends AbstractMessage { @@ -36,10 +35,7 @@ public final class OpenComputerTerminalMessage extends AbstractMessage { @Override protected void handleMessage(final NetworkEvent.Context context) { - final ServerPlayer player = context.getSender(); - if (player != null) { - MessageUtils.withNearbyServerBlockEntityAt(context, pos, ComputerBlockEntity.class, - computer -> computer.openTerminalScreen(player)); - } + MessageUtils.withNearbyServerBlockEntityForInteraction(context, pos, ComputerBlockEntity.class, + (player, computer) -> computer.openTerminalScreen(player)); } } diff --git a/src/main/java/li/cil/oc2/common/network/message/ProjectorFramebufferMessage.java b/src/main/java/li/cil/oc2/common/network/message/ProjectorFramebufferMessage.java index 6bc481c8..ce26c42c 100644 --- a/src/main/java/li/cil/oc2/common/network/message/ProjectorFramebufferMessage.java +++ b/src/main/java/li/cil/oc2/common/network/message/ProjectorFramebufferMessage.java @@ -14,8 +14,8 @@ public final class ProjectorFramebufferMessage extends AbstractMessage { /////////////////////////////////////////////////////////////////// - public ProjectorFramebufferMessage(final ProjectorBlockEntity projector, final ByteBuffer frame) { - this.pos = projector.getBlockPos(); + public ProjectorFramebufferMessage(final BlockPos projectorPos, final ByteBuffer frame) { + this.pos = projectorPos; this.frame = frame; } @@ -44,8 +44,7 @@ public final class ProjectorFramebufferMessage extends AbstractMessage { @Override protected void handleMessage(final NetworkEvent.Context context) { - context.enqueueWork(() -> - MessageUtils.withClientBlockEntityAt(pos, ProjectorBlockEntity.class, - projector -> projector.applyNextFrame(frame))); + MessageUtils.withClientBlockEntityAt(pos, ProjectorBlockEntity.class, + projector -> projector.applyNextFrame(frame)); } } diff --git a/src/main/java/li/cil/oc2/common/network/message/ProjectorRequestFramebufferMessage.java b/src/main/java/li/cil/oc2/common/network/message/ProjectorRequestFramebufferMessage.java new file mode 100644 index 00000000..e7477a50 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/network/message/ProjectorRequestFramebufferMessage.java @@ -0,0 +1,42 @@ +package li.cil.oc2.common.network.message; + +import li.cil.oc2.common.blockentity.ProjectorBlockEntity; +import li.cil.oc2.common.network.MessageUtils; +import li.cil.oc2.common.network.ProjectorLoadBalancer; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraftforge.network.NetworkEvent; + +public final class ProjectorRequestFramebufferMessage extends AbstractMessage { + private BlockPos pos; + + /////////////////////////////////////////////////////////////////// + + public ProjectorRequestFramebufferMessage(final ProjectorBlockEntity projector) { + this.pos = projector.getBlockPos(); + } + + public ProjectorRequestFramebufferMessage(final FriendlyByteBuf buffer) { + super(buffer); + } + + /////////////////////////////////////////////////////////////////// + + @Override + public void fromBytes(final FriendlyByteBuf buffer) { + pos = buffer.readBlockPos(); + } + + @Override + public void toBytes(final FriendlyByteBuf buffer) { + buffer.writeBlockPos(pos); + } + + /////////////////////////////////////////////////////////////////// + + @Override + protected void handleMessage(final NetworkEvent.Context context) { + MessageUtils.withNearbyServerBlockEntity(context, pos, ProjectorBlockEntity.class, + (player, projector) -> ProjectorLoadBalancer.updateWatcher(projector, player)); + } +} diff --git a/src/main/java/li/cil/oc2/common/vm/device/SimpleFramebufferDevice.java b/src/main/java/li/cil/oc2/common/vm/device/SimpleFramebufferDevice.java index 944fca02..053e7cb2 100644 --- a/src/main/java/li/cil/oc2/common/vm/device/SimpleFramebufferDevice.java +++ b/src/main/java/li/cil/oc2/common/vm/device/SimpleFramebufferDevice.java @@ -35,6 +35,7 @@ public final class SimpleFramebufferDevice implements MemoryMappedDevice { this.buffer = buffer.order(ByteOrder.LITTLE_ENDIAN); this.dirtyLines = new BitSet(height / 2); + this.dirtyLines.set(0, height / 2); } /////////////////////////////////////////////////////////////// @@ -52,6 +53,10 @@ public final class SimpleFramebufferDevice implements MemoryMappedDevice { return height; } + public boolean hasChanges() { + return !dirtyLines.isEmpty(); + } + public boolean applyChanges(final Picture picture) { if (dirtyLines.isEmpty()) { return false;