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:
johni0702
2015-07-15 14:21:18 +02:00
parent 3a2c35cb22
commit 7fd429e25f
44 changed files with 1142 additions and 1244 deletions

View File

@@ -1,63 +1,44 @@
package eu.crushedpixel.replaymod.video;
import com.google.common.base.Preconditions;
import eu.crushedpixel.replaymod.settings.RenderOptions;
import eu.crushedpixel.replaymod.utils.ByteBufferPool;
import eu.crushedpixel.replaymod.utils.StreamPipe;
import eu.crushedpixel.replaymod.utils.StringUtils;
import eu.crushedpixel.replaymod.video.frame.ARGBFrame;
import eu.crushedpixel.replaymod.video.rendering.FrameConsumer;
import net.minecraft.client.Minecraft;
import net.minecraft.crash.CrashReport;
import net.minecraft.crash.CrashReportCategory;
import org.apache.commons.io.IOUtils;
import org.lwjgl.util.ReadableDimension;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferInt;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
import static org.apache.commons.lang3.Validate.isTrue;
public class VideoWriter {
public class VideoWriter implements FrameConsumer<ARGBFrame> {
private final RenderOptions options;
private final Process process;
private final OutputStream outputStream;
private final WritableByteChannel channel;
private final String commandArgs;
private volatile boolean active = true;
private volatile boolean cancelled = false;
private int queueLimit;
private final Queue<BufferedImage> toWrite;
private final Thread writerThread;
private final Lock lock = new ReentrantLock();
private final Condition emptyCondition = lock.newCondition();
private final Condition noLongerEmptyCondition = lock.newCondition();
private final Condition noLongerFullCondition = lock.newCondition();
public VideoWriter(final VideoRenderer renderer, final RenderOptions options) throws IOException {
public VideoWriter(final RenderOptions options) throws IOException {
this.options = options.copy();
this.queueLimit = options.getWriterQueueSize();
this.toWrite = new LinkedList<BufferedImage>();
File outputFolder = options.getOutputFile().getParentFile();
String fileName = options.getOutputFile().getName();
final String args = options.getExportCommandArgs()
commandArgs = options.getExportCommandArgs()
.replace("%WIDTH%", String.valueOf(options.getWidth()))
.replace("%HEIGHT%", String.valueOf(options.getHeight()))
.replace("%FPS%", String.valueOf(options.getFps()))
@@ -66,127 +47,63 @@ public class VideoWriter {
List<String> command = new ArrayList<String>();
command.add(options.getExportCommand());
command.addAll(StringUtils.translateCommandline(args));
System.out.println("Starting " + options.getExportCommand() + " with args: " + args);
command.addAll(StringUtils.translateCommandline(commandArgs));
System.out.println("Starting " + options.getExportCommand() + " with args: " + commandArgs);
process = new ProcessBuilder(command).directory(outputFolder).start();
OutputStream exportLogOut = new FileOutputStream("export.log");
new StreamPipe(process.getInputStream(), exportLogOut).start();
new StreamPipe(process.getErrorStream(), exportLogOut).start();
outputStream = process.getOutputStream();
channel = Channels.newChannel(outputStream);
}
writerThread = new Thread(new Runnable() {
@Override
public void close() throws IOException {
IOUtils.closeQuietly(outputStream);
@Override
public void run() {
try {
while (!cancelled && (active || !toWrite.isEmpty())) {
try {
lock.lockInterruptibly();
BufferedImage img;
try {
img = toWrite.poll();
if (img == null) {
noLongerEmptyCondition.await();
img = toWrite.poll();
}
noLongerFullCondition.signal();
if (toWrite.isEmpty()) {
emptyCondition.signalAll();
}
} finally {
lock.unlock();
}
DataBuffer imgBuffer = img.getRaster().getDataBuffer();
if (imgBuffer instanceof DataBufferByte) {
outputStream.write(((DataBufferByte) imgBuffer).getData());
} else if (imgBuffer instanceof DataBufferInt) {
ByteBuffer byteBuffer = ByteBuffer.allocate(img.getWidth() * img.getHeight() * 4);
byteBuffer.asIntBuffer().put(((DataBufferInt) imgBuffer).getData());
channel.write(byteBuffer);
} else {
throw new RuntimeException("DataBuffer type not supported: " + imgBuffer.getClass());
}
} catch (InterruptedException ignored) {
}
long startTime = System.nanoTime();
long rem = TimeUnit.SECONDS.toNanos(30);
do {
try {
process.exitValue();
break;
} catch(IllegalThreadStateException ex) {
if (rem > 0) {
try {
Thread.sleep(Math.min(TimeUnit.NANOSECONDS.toMillis(rem) + 1, 100));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
toWrite.clear();
IOUtils.closeQuietly(outputStream);
} catch (IOException e) {
if (active) {
CrashReport report = new CrashReport("Exporting frame", e);
CrashReportCategory exportDetails = report.makeCategory("Export details");
exportDetails.addCrashSection("Export command", options.getExportCommand());
exportDetails.addCrashSection("Export args", args);
Minecraft.getMinecraft().crashed(report);
renderer.cancel();
}
} finally {
process.destroy();
}
}
}, "replaymod-video-writer");
writerThread.start();
rem = TimeUnit.SECONDS.toNanos(30) - (System.nanoTime() - startTime);
} while (rem > 0);
process.destroy();
}
/**
* Add the image to the writer queue.
* @param image The image
* @param waitIfFull Whether to wait if the queue is full or to return immediately
* @return {@code} true if the image was added, {@code false} if it could not be added due to the queue being full
* and {@code waitIfFull} being {@code false}
*/
public boolean writeImage(BufferedImage image, boolean waitIfFull) {
Preconditions.checkState(active, "This VideoWriter has already been closed.");
isTrue(image.getWidth() == options.getWidth(), "Width has to be " + options.getWidth() + " but was " + image.getWidth());
isTrue(image.getHeight() == options.getHeight(), "Height has to be " + options.getHeight() + " but was " + image.getHeight());
lock.lock();
@Override
public void consume(ARGBFrame frame) {
try {
while (toWrite.size() >= queueLimit) {
if (waitIfFull) {
noLongerFullCondition.await();
} else {
return false;
}
}
toWrite.offer(image);
noLongerEmptyCondition.signal();
return true;
} catch (InterruptedException ignored) {
} finally {
lock.unlock();
}
return false;
}
public void endRecording() {
active = false;
writerThread.interrupt();
}
public void abortRecording() {
cancelled = true;
active = false;
writerThread.interrupt();
}
public void waitTillQueueEmpty() {
lock.lock();
try {
if (!toWrite.isEmpty()) {
emptyCondition.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
checkSize(frame.getSize());
channel.write(frame.getByteBuffer());
ByteBufferPool.release(frame.getByteBuffer());
} catch (Throwable t) {
CrashReport report = CrashReport.makeCrashReport(t, "Exporting frame");
CrashReportCategory exportDetails = report.makeCategory("Export details");
exportDetails.addCrashSection("Export command", options.getExportCommand());
exportDetails.addCrashSection("Export args", commandArgs);
Minecraft.getMinecraft().crashed(report);
}
}
public void waitForFinish() throws InterruptedException {
Preconditions.checkState(!active, "Video writer still active.");
writerThread.join();
private void checkSize(ReadableDimension size) {
checkSize(size.getWidth(), size.getHeight());
}
private void checkSize(int width, int height) {
isTrue(width == options.getWidth(), "Width has to be %d but was %d", options.getWidth(), width);
isTrue(height == options.getHeight(), "Height has to be %d but was %d", options.getHeight(), height);
}
}