Run all sending of framebuffer data through a load balancer, so we have more control over bandwidth use.
Penalize projectors that a) send to multiple clients, b) are far away from players.
This commit is contained in:
@@ -231,6 +231,8 @@ public final class ProjectorDepthRenderer {
|
||||
renderProjectorDepthBuffer(minecraft, partialTicks, startNanos, viewModelStack);
|
||||
|
||||
storeProjectorColorBuffer(projectorIndex, projector);
|
||||
|
||||
projector.onRendering();
|
||||
}
|
||||
} finally {
|
||||
finishDepthBufferRendering(minecraft, player);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,13 +44,15 @@ public final class ProjectorVMDevice extends IdentityProxy<ProjectorBlockEntity>
|
||||
|
||||
///////////////////////////////////////////////////////////////
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <T extends BlockEntity> void withNearbyServerBlockEntityAt(final NetworkEvent.Context context, final BlockPos pos, final Class<T> type, final Consumer<T> callback) {
|
||||
public static <T extends BlockEntity> void withNearbyServerBlockEntityForInteraction(final NetworkEvent.Context context, final BlockPos pos, final Class<T> type, final BiConsumer<ServerPlayer, T> callback) {
|
||||
final ServerPlayer player = context.getSender();
|
||||
if (player == null || !pos.closerThan(player.position(), 8)) {
|
||||
return;
|
||||
}
|
||||
|
||||
withNearbyServerBlockEntity(context, pos, type, callback);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T extends BlockEntity> void withNearbyServerBlockEntity(final NetworkEvent.Context context, final BlockPos pos, final Class<T> type, final BiConsumer<ServerPlayer, T> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
* <p>
|
||||
* We try to satisfy two limits here:
|
||||
* <ul>
|
||||
* <li>
|
||||
* Server side, limiting overall bandwidth consumed by projectors.
|
||||
* </li>
|
||||
* <li>
|
||||
* Client side, limiting per-client bandwidth consumed by projectors.
|
||||
* </li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* This is used to let projectors register new frames to be sent, and to register new players
|
||||
* with already being tracked projectors.
|
||||
* <p>
|
||||
* 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<ProjectorBlockEntity, ProjectorInfo> 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* Ignored if there are no players watching the projector.
|
||||
*/
|
||||
public static void offerFrame(final ProjectorBlockEntity projector, final Supplier<ByteBuffer> 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<ProjectorBlockEntity, ProjectorInfo> 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<ServerPlayer, Long> players = new WeakHashMap<>();
|
||||
|
||||
/**
|
||||
* The current penalty, in the form of rounds in the round-robin to skip.
|
||||
*/
|
||||
private int skipCount;
|
||||
@Nullable private Supplier<ByteBuffer> 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<ByteBuffer> 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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user