Rework rendering pipeline to make better use of multithreading
Move OpenGL frame to ARGB conversion to processing threads Move video exporting to processing threads Skip creation of BufferedImage and instead use ByteBuffer with ARGB content directly
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
package eu.crushedpixel.replaymod.video.rendering;
|
||||
|
||||
public interface Frame {
|
||||
int getFrameId();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package eu.crushedpixel.replaymod.video.rendering;
|
||||
|
||||
import java.io.Closeable;
|
||||
|
||||
public interface FrameCapturer<R extends Frame> extends Closeable {
|
||||
|
||||
boolean isDone();
|
||||
|
||||
R process();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package eu.crushedpixel.replaymod.video.rendering;
|
||||
|
||||
import java.io.Closeable;
|
||||
|
||||
public interface FrameConsumer<P extends Frame> extends Closeable {
|
||||
|
||||
void consume(P frame);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package eu.crushedpixel.replaymod.video.rendering;
|
||||
|
||||
import java.io.Closeable;
|
||||
|
||||
public interface FrameProcessor<R extends Frame, P extends Frame> extends Closeable {
|
||||
|
||||
P process(R rawFrame);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package eu.crushedpixel.replaymod.video.rendering;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.crash.CrashReport;
|
||||
import net.minecraft.util.ReportedException;
|
||||
import org.lwjgl.opengl.Display;
|
||||
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class Pipeline<R extends Frame, P extends Frame> implements Runnable {
|
||||
|
||||
private final FrameCapturer<R> capturer;
|
||||
private final FrameProcessor<R, P> processor;
|
||||
private int consumerNextFrame;
|
||||
private final Object consumerLock = new Object();
|
||||
private final FrameConsumer<P> consumer;
|
||||
|
||||
private Thread runningThread;
|
||||
|
||||
public Pipeline(FrameCapturer<R> capturer, FrameProcessor<R, P> processor, FrameConsumer<P> consumer) {
|
||||
this.capturer = capturer;
|
||||
this.processor = processor;
|
||||
this.consumer = consumer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void run() {
|
||||
runningThread = Thread.currentThread();
|
||||
consumerNextFrame = 0;
|
||||
int processors = Runtime.getRuntime().availableProcessors();
|
||||
int processThreads = Math.max(1, processors - 2); // One processor for the main thread and one for ffmpeg, sorry OS :(
|
||||
ExecutorService processService = new ThreadPoolExecutor(processThreads, processThreads,
|
||||
0L, TimeUnit.MILLISECONDS,
|
||||
new ArrayBlockingQueue<Runnable>(2) {
|
||||
@Override
|
||||
public boolean offer(Runnable runnable) {
|
||||
try {
|
||||
put(runnable);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}, new ThreadPoolExecutor.DiscardPolicy());
|
||||
|
||||
Minecraft mc = Minecraft.getMinecraft();
|
||||
while (!capturer.isDone() && !Thread.currentThread().isInterrupted()) {
|
||||
if (Display.isCloseRequested() || mc.hasCrashed) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
R rawFrame = capturer.process();
|
||||
if (rawFrame != null) {
|
||||
processService.submit(new ProcessTask(rawFrame));
|
||||
}
|
||||
}
|
||||
|
||||
processService.shutdown();
|
||||
try {
|
||||
processService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
|
||||
try {
|
||||
capturer.close();
|
||||
processor.close();
|
||||
consumer.close();
|
||||
} catch (Throwable t) {
|
||||
CrashReport crashReport = CrashReport.makeCrashReport(t, "Cleaning up rendering pipeline");
|
||||
throw new ReportedException(crashReport);
|
||||
}
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
if (runningThread != null) {
|
||||
runningThread.interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
private class ProcessTask implements Runnable {
|
||||
private final R rawFrame;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
P processedFrame = processor.process(rawFrame);
|
||||
synchronized (consumerLock) {
|
||||
while (consumerNextFrame != processedFrame.getFrameId()) {
|
||||
try {
|
||||
consumerLock.wait();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
consumer.consume(processedFrame);
|
||||
consumerNextFrame++;
|
||||
consumerLock.notifyAll();
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
CrashReport crashReport = CrashReport.makeCrashReport(t, "Processing frame");
|
||||
Minecraft.getMinecraft().crashed(crashReport);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package eu.crushedpixel.replaymod.video.rendering;
|
||||
|
||||
import eu.crushedpixel.replaymod.settings.RenderOptions;
|
||||
import eu.crushedpixel.replaymod.video.capturer.*;
|
||||
import eu.crushedpixel.replaymod.video.entity.CubicEntityRenderer;
|
||||
import eu.crushedpixel.replaymod.video.entity.CustomEntityRenderer;
|
||||
import eu.crushedpixel.replaymod.video.entity.StereoscopicEntityRenderer;
|
||||
import eu.crushedpixel.replaymod.video.frame.ARGBFrame;
|
||||
import eu.crushedpixel.replaymod.video.frame.CubicOpenGlFrame;
|
||||
import eu.crushedpixel.replaymod.video.frame.OpenGlFrame;
|
||||
import eu.crushedpixel.replaymod.video.frame.StereoscopicOpenGlFrame;
|
||||
import eu.crushedpixel.replaymod.video.processor.CubicToARGBProcessor;
|
||||
import eu.crushedpixel.replaymod.video.processor.EquirectangularToARGBProcessor;
|
||||
import eu.crushedpixel.replaymod.video.processor.OpenGlToARGBProcessor;
|
||||
import eu.crushedpixel.replaymod.video.processor.StereoscopicToARGBProcessor;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@UtilityClass
|
||||
public class Pipelines {
|
||||
public enum Preset {
|
||||
DEFAULT, STEREOSCOPIC, CUBIC, EQUIRECTANGULAR
|
||||
}
|
||||
|
||||
public static Pipeline newPipeline(Preset preset, RenderInfo renderInfo, FrameConsumer<ARGBFrame> consumer) {
|
||||
switch (preset) {
|
||||
case DEFAULT:
|
||||
return newDefaultPipeline(renderInfo, consumer);
|
||||
case STEREOSCOPIC:
|
||||
return newStereoscopicPipeline(renderInfo, consumer);
|
||||
case CUBIC:
|
||||
return newCubicPipeline(renderInfo, consumer);
|
||||
case EQUIRECTANGULAR:
|
||||
return newEquirectangularPipeline(renderInfo, consumer);
|
||||
}
|
||||
throw new UnsupportedOperationException("Unknown preset: " + preset);
|
||||
}
|
||||
|
||||
public static Pipeline<OpenGlFrame, ARGBFrame> newDefaultPipeline(RenderInfo renderInfo, FrameConsumer<ARGBFrame> consumer) {
|
||||
RenderOptions options = renderInfo.getRenderOptions();
|
||||
return new Pipeline<OpenGlFrame, ARGBFrame>(
|
||||
new SimpleOpenGlFrameCapturer(new CustomEntityRenderer<CaptureData>(options), renderInfo),
|
||||
new OpenGlToARGBProcessor(),
|
||||
consumer
|
||||
);
|
||||
}
|
||||
|
||||
public static Pipeline<StereoscopicOpenGlFrame, ARGBFrame> newStereoscopicPipeline(RenderInfo renderInfo, FrameConsumer<ARGBFrame> consumer) {
|
||||
RenderOptions options = renderInfo.getRenderOptions();
|
||||
return new Pipeline<StereoscopicOpenGlFrame, ARGBFrame>(
|
||||
new StereoscopicOpenGlFrameCapturer(new StereoscopicEntityRenderer(options), renderInfo),
|
||||
new StereoscopicToARGBProcessor(),
|
||||
consumer
|
||||
);
|
||||
}
|
||||
|
||||
public static Pipeline<CubicOpenGlFrame, ARGBFrame> newCubicPipeline(RenderInfo renderInfo, FrameConsumer<ARGBFrame> consumer) {
|
||||
RenderOptions options = renderInfo.getRenderOptions();
|
||||
return new Pipeline<CubicOpenGlFrame, ARGBFrame>(
|
||||
new CubicOpenGlFrameCapturer(new CubicEntityRenderer(options), renderInfo, options.getWidth() / 4),
|
||||
new CubicToARGBProcessor(),
|
||||
consumer
|
||||
);
|
||||
}
|
||||
|
||||
public static Pipeline<CubicOpenGlFrame, ARGBFrame> newEquirectangularPipeline(RenderInfo renderInfo, FrameConsumer<ARGBFrame> consumer) {
|
||||
RenderOptions options = renderInfo.getRenderOptions();
|
||||
return new Pipeline<CubicOpenGlFrame, ARGBFrame>(
|
||||
new CubicOpenGlFrameCapturer(new CubicEntityRenderer(options), renderInfo, options.getWidth() / 4),
|
||||
new EquirectangularToARGBProcessor(options.getWidth() / 4),
|
||||
consumer
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user