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

@@ -22,11 +22,12 @@ import eu.crushedpixel.replaymod.settings.RenderOptions;
import eu.crushedpixel.replaymod.settings.ReplaySettings;
import eu.crushedpixel.replaymod.sound.SoundHandler;
import eu.crushedpixel.replaymod.timer.ReplayTimer;
import eu.crushedpixel.replaymod.utils.OpenGLUtils;
import eu.crushedpixel.replaymod.utils.ReplayFile;
import eu.crushedpixel.replaymod.utils.ReplayFileIO;
import eu.crushedpixel.replaymod.utils.TooltipRenderer;
import eu.crushedpixel.replaymod.video.frame.*;
import lombok.Getter;
import eu.crushedpixel.replaymod.video.rendering.Pipelines;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.entity.RenderPlayer;
import net.minecraft.client.resources.IResourcePack;
@@ -110,6 +111,10 @@ public class ReplayMod {
e.printStackTrace();
}
// Initialize the static OpenGL info field from the minecraft main thread
// Unfortunately lwjgl uses static methods so we have to make use of magic init calls as well
OpenGLUtils.init();
config = new Configuration(event.getSuggestedConfigurationFile());
config.load();
AuthenticationHandler.loadAuthkeyFromConfig();
@@ -280,31 +285,29 @@ public class ReplayMod {
options.setExportCommandArgs(exportCommandArgs);
}
FrameRenderer renderer;
Pipelines.Preset pipelinePreset = Pipelines.Preset.DEFAULT;
if (type != null) {
String[] parts = type.split(":");
type = parts[0].toUpperCase();
if ("NORMAL".equals(type) || "DEFAULT".equals(type)) {
renderer = new DefaultFrameRenderer(options);
pipelinePreset = Pipelines.Preset.DEFAULT;
} else if ("STEREO".equals(type) || "STEREOSCOPIC".equals(type)) {
renderer = new StereoscopicFrameRenderer(options);
pipelinePreset = Pipelines.Preset.STEREOSCOPIC;
} else if ("CUBIC".equals(type)) {
if (parts.length < 2) {
throw new IllegalArgumentException("Cubic renderer requires boolean for whether it's stable.");
}
renderer = new CubicFrameRenderer(options, Boolean.parseBoolean(parts[1]));
pipelinePreset = Pipelines.Preset.CUBIC;
} else if ("EQUIRECTANGULAR".equals(type)) {
if (parts.length < 2) {
throw new IllegalArgumentException("Equirectangular renderer requires boolean for whether it's stable.");
}
renderer = new EquirectangularFrameRenderer(options, Boolean.parseBoolean(parts[1]));
pipelinePreset = Pipelines.Preset.EQUIRECTANGULAR;
} else {
throw new IllegalArgumentException("Unknown type: " + parts[0]);
}
} else {
renderer = new DefaultFrameRenderer(options);
}
options.setRenderer(renderer);
options.setMode(pipelinePreset);
@SuppressWarnings("unchecked")
Queue<ListenableFutureTask> tasks = mc.scheduledTasks;

View File

@@ -9,7 +9,7 @@ import eu.crushedpixel.replaymod.settings.RenderOptions;
import eu.crushedpixel.replaymod.utils.MouseUtils;
import eu.crushedpixel.replaymod.utils.ReplayFileIO;
import eu.crushedpixel.replaymod.utils.StringUtils;
import eu.crushedpixel.replaymod.video.frame.*;
import eu.crushedpixel.replaymod.video.rendering.Pipelines;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiButton;
import net.minecraft.client.gui.GuiErrorScreen;
@@ -449,8 +449,6 @@ public class GuiRenderSettings extends GuiScreen {
}
private void startRendering() {
FrameRenderer renderer = null;
RendererSettings r = rendererDropdown.getElement(rendererDropdown.getSelectionIndex());
RenderOptions options = new RenderOptions();
@@ -477,16 +475,19 @@ public class GuiRenderSettings extends GuiScreen {
options.setWidth(getWidthSetting());
options.setHeight(getHeightSetting());
Pipelines.Preset pipePreset = Pipelines.Preset.DEFAULT;
if(r == RendererSettings.DEFAULT) {
renderer = new DefaultFrameRenderer(options);
pipePreset = Pipelines.Preset.DEFAULT;
} else if(r == RendererSettings.STEREOSCOPIC) {
renderer = new StereoscopicFrameRenderer(options);
pipePreset = Pipelines.Preset.STEREOSCOPIC;
} else if(r == RendererSettings.CUBIC) {
renderer = new CubicFrameRenderer(options, ignoreCamDir.isChecked());
pipePreset = Pipelines.Preset.CUBIC;
} else if(r == RendererSettings.EQUIRECTANGULAR) {
renderer = new EquirectangularFrameRenderer(options, ignoreCamDir.isChecked());
pipePreset = Pipelines.Preset.EQUIRECTANGULAR;
}
options.setRenderer(renderer);
options.setMode(pipePreset);
options.setIgnoreCameraRotation(ignoreCamDir.isChecked());
if(commandInput.getText().trim().length() > 0) {
options.setExportCommand(commandInput.getText().trim());
@@ -525,6 +526,9 @@ public class GuiRenderSettings extends GuiScreen {
ignoreCamDir.enabled = true;
}
if (!ignoreCamDir.enabled) {
ignoreCamDir.setIsChecked(false);
}
xRes.setCursorPositionEnd();
xRes.setSelectionPos(xRes.getCursorPosition());

View File

@@ -1,18 +1,32 @@
package eu.crushedpixel.replaymod.gui;
import eu.crushedpixel.replaymod.gui.elements.GuiProgressBar;
import eu.crushedpixel.replaymod.utils.BoundingUtils;
import eu.crushedpixel.replaymod.utils.DurationUtils;
import eu.crushedpixel.replaymod.video.VideoRenderer;
import eu.crushedpixel.replaymod.video.frame.FrameRenderer;
import eu.crushedpixel.replaymod.video.frame.ARGBFrame;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiButton;
import net.minecraft.client.gui.GuiScreen;
import net.minecraft.client.renderer.texture.DynamicTexture;
import net.minecraft.client.renderer.texture.TextureUtil;
import net.minecraft.client.resources.I18n;
import net.minecraft.util.ResourceLocation;
import net.minecraftforge.fml.client.config.GuiCheckBox;
import org.lwjgl.util.Dimension;
import org.lwjgl.util.ReadableDimension;
import java.awt.*;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import static net.minecraft.client.renderer.GlStateManager.bindTexture;
import static net.minecraft.client.renderer.GlStateManager.color;
public class GuiVideoRenderer extends GuiScreen {
private static final ResourceLocation noPreviewTexture = new ResourceLocation("replaymod", "logo.jpg");
private final VideoRenderer renderer;
private final String SCREEN_TITLE = I18n.format("replaymod.gui.rendering.title");
@@ -27,6 +41,9 @@ public class GuiVideoRenderer extends GuiScreen {
private GuiCheckBox previewCheckBox;
private GuiProgressBar progressBar;
private DynamicTexture previewTexture;
private boolean previewTextureDirty;
private int renderTimeTaken = 0;
private long prevTime = -1;
private long prevRenderTime = -1;
@@ -87,8 +104,6 @@ public class GuiVideoRenderer extends GuiScreen {
} else if (CANCEL_CONFIRM.equals(cancelButton.displayString)) {
renderer.cancel();
}
} else if (button == previewCheckBox) {
renderer.getFrameRenderer().setPreviewActive(previewCheckBox.isChecked());
}
}
@@ -139,7 +154,6 @@ public class GuiVideoRenderer extends GuiScreen {
String takenString = I18n.format("replaymod.gui.rendering.timetaken")+": "+DurationUtils.convertSecondsToString(renderTimeTaken/1000);
String leftString = I18n.format("replaymod.gui.rendering.timeleft")+": "+DurationUtils.convertSecondsToString(renderTimeLeft);
FrameRenderer frameRenderer = renderer.getFrameRenderer();
int centerX = width / 2;
drawBackground(0);
@@ -159,9 +173,9 @@ public class GuiVideoRenderer extends GuiScreen {
int previewY = previewCheckBox.yPosition - 10 - previewHeight;
if(previewCheckBox.isChecked()) {
frameRenderer.renderPreview(previewX, previewY, previewWidth, previewHeight);
renderPreview(previewX, previewY, previewWidth, previewHeight);
} else {
FrameRenderer.renderNoPreview(previewX, previewY, previewWidth, previewHeight);
renderNoPreview(previewX, previewY, previewWidth, previewHeight);
}
drawString(fontRendererObj, takenString, 12, previewCheckBox.yPosition + 5 + 20, Color.WHITE.getRGB());
@@ -170,4 +184,75 @@ public class GuiVideoRenderer extends GuiScreen {
super.drawScreen(mouseX, mouseY, partialTicks);
}
private synchronized void renderPreview(int x, int y, int width, int height) {
ReadableDimension videoSize = renderer.getFrameSize();
final int videoWidth = videoSize.getWidth();
final int videoHeight = videoSize.getHeight();
if (previewTexture == null) {
previewTexture = new DynamicTexture(videoWidth, videoHeight) {
@Override
public void updateDynamicTexture() {
bindTexture(getGlTextureId());
TextureUtil.uploadTextureSub(0, getTextureData(), videoWidth, videoHeight, 0, 0, true, false, false);
}
};
}
if (previewTextureDirty) {
previewTexture.updateDynamicTexture();
previewTextureDirty = false;
}
Dimension dimension = BoundingUtils.fitIntoBounds(new Dimension(videoSize), new Dimension(width, height));
x += (width - dimension.getWidth()) / 2;
y += (height - dimension.getHeight()) / 2;
width = dimension.getWidth();
height = dimension.getHeight();
color(1, 1, 1, 1);
bindTexture(previewTexture.getGlTextureId());
drawScaledCustomSizeModalRect(x, y, 0, 0, videoWidth, videoHeight, width, height, videoWidth, videoHeight);
}
private void renderNoPreview(int x, int y, int width, int height) {
int actualWidth = width;
int actualHeight = height;
if (width / height > 1280 / 720) {
actualWidth = height * 1280 / 720;
} else {
actualHeight = width * 720 / 1280;
}
x += (width - actualWidth) / 2;
y += (height - actualHeight) / 2;
Minecraft.getMinecraft().getTextureManager().bindTexture(noPreviewTexture);
color(1, 1, 1, 1);
drawScaledCustomSizeModalRect(x, y, 0, 0, 1280, 720, actualWidth, actualHeight, 1280, 720);
}
public void updatePreview(ARGBFrame frame) {
if (previewCheckBox.isChecked() && previewTexture != null) {
ByteBuffer buffer = frame.getByteBuffer();
buffer.mark();
synchronized (this) {
swapEndianness(buffer);
buffer.asIntBuffer().get(previewTexture.getTextureData());
swapEndianness(buffer);
previewTextureDirty = true;
}
buffer.reset();
}
}
private void swapEndianness(ByteBuffer buffer) {
if (buffer.order() == ByteOrder.BIG_ENDIAN) {
buffer.order(ByteOrder.LITTLE_ENDIAN);
} else {
buffer.order(ByteOrder.BIG_ENDIAN);
}
}
}

View File

@@ -1,25 +1,23 @@
package eu.crushedpixel.replaymod.settings;
import eu.crushedpixel.replaymod.utils.ReplayFileIO;
import eu.crushedpixel.replaymod.video.frame.FrameRenderer;
import eu.crushedpixel.replaymod.video.rendering.Pipelines;
import lombok.Data;
import net.minecraft.client.Minecraft;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import static org.apache.commons.lang3.Validate.isTrue;
import static org.apache.commons.lang3.Validate.notNull;
@Data
public final class RenderOptions {
private FrameRenderer renderer;
private Pipelines.Preset mode = Pipelines.Preset.DEFAULT;
private String bitrate = "10M";
private int fps = 30;
private File outputFile;
private boolean ignoreCameraRotation;
// Advanced
private boolean waitForChunks = true;
private boolean isLinearMovement = false;
@@ -32,11 +30,6 @@ public final class RenderOptions {
private String exportCommandArgs = "-f rawvideo -pix_fmt argb -s %WIDTH%x%HEIGHT% -r %FPS% -i - " +
"-an " +
"-c:v libvpx -b:v %BITRATE% %FILENAME%";
private int writerQueueSize = Integer.parseInt(System.getProperty("replaymod.render.writerQueueSize", "1"));
public void setRenderer(FrameRenderer renderer) {
this.renderer = notNull(renderer);
}
public int getFps() {
return fps;
@@ -53,9 +46,10 @@ public final class RenderOptions {
public RenderOptions copy() {
RenderOptions copy = new RenderOptions();
copy.renderer = this.renderer;
copy.mode = this.mode;
copy.bitrate = this.bitrate;
copy.fps = this.fps;
copy.ignoreCameraRotation = this.ignoreCameraRotation;
copy.waitForChunks = this.waitForChunks;
copy.isLinearMovement = this.isLinearMovement;
copy.skyColor = this.skyColor;
@@ -63,7 +57,6 @@ public final class RenderOptions {
copy.height = this.height;
copy.exportCommand = this.exportCommand;
copy.exportCommandArgs = this.exportCommandArgs;
copy.writerQueueSize = this.writerQueueSize;
return copy;
}
}

View File

@@ -0,0 +1,48 @@
package eu.crushedpixel.replaymod.utils;
import com.google.common.collect.Maps;
import org.lwjgl.BufferUtils;
import java.lang.ref.SoftReference;
import java.nio.ByteBuffer;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
public class ByteBufferPool {
private static Map<Integer, List<SoftReference<ByteBuffer>>> bufferPool = Maps.newHashMap();
public static synchronized ByteBuffer allocate(int size) {
List<SoftReference<ByteBuffer>> available = bufferPool.get(size);
if (available != null) {
Iterator<SoftReference<ByteBuffer>> iter = available.iterator();
try {
while (iter.hasNext()) {
SoftReference<ByteBuffer> reference = iter.next();
ByteBuffer buffer = reference.get();
iter.remove();
if (buffer != null) {
return buffer;
}
}
} finally {
if (!iter.hasNext()) {
bufferPool.remove(size);
}
}
}
return BufferUtils.createByteBuffer(size);
}
public static synchronized void release(ByteBuffer buffer) {
buffer.clear();
int size = buffer.capacity();
List<SoftReference<ByteBuffer>> available = bufferPool.get(size);
if (available == null) {
available = new LinkedList<SoftReference<ByteBuffer>>();
bufferPool.put(size, available);
}
available.add(new SoftReference<ByteBuffer>(buffer));
}
}

View File

@@ -3,8 +3,6 @@ package eu.crushedpixel.replaymod.utils;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
@@ -19,23 +17,28 @@ public class OpenGLUtils {
VIEWPORT_MAX_HEIGHT = buffer.get();
}
public static void openGlBytesToBufferedImage(ByteBuffer buffer, int bufferWidth, BufferedImage image, int offsetX, int offsetY) {
int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
int imageWidth = image.getWidth();
/**
* Magic init method which has to be called from the OpenGL thread so the variables in this class
* can be initialized successfully.
* Does not perform any work on its own.
*/
public static void init() {
}
public static void openGlBytesToARBG(ByteBuffer buffer, int bufferWidth, int xOffset, int yOffset, ByteBuffer to, int width) {
byte[] pixel = new byte[4];
pixel[0] = (byte) 0xff;
int bufferSize = buffer.remaining() / 3;
// Read the OpenGL image row by row from right to left (flipped horizontally)
for (int i = bufferSize - 1; i >= 0; i--) {
// Coordinates in the final image
int x = offsetX + bufferWidth - i % bufferWidth - 1; // X coord of OpenGL image has to be flipped first
int y = offsetY + i / bufferWidth;
if (x >= imageWidth || y * imageWidth >= pixels.length) {
buffer.position(buffer.position() + 3); // Pixel would end up outside of image
continue;
}
int x = xOffset + bufferWidth - i % bufferWidth - 1; // X coord of OpenGL image has to be flipped first
int y = yOffset + i / bufferWidth;
// Write to image (row by row, left to right)
pixels[y * imageWidth + x] = 0xff << 24 | (buffer.get() & 0xff) << 16 | (buffer.get() & 0xff) << 8 | buffer.get() & 0xff;
buffer.get(pixel, 1, 3);
to.position((y * width + x) * 4);
to.put(pixel);
}
buffer.rewind();
to.rewind();
}
}

View File

@@ -16,14 +16,18 @@ import eu.crushedpixel.replaymod.replay.ReplaySender;
import eu.crushedpixel.replaymod.settings.RenderOptions;
import eu.crushedpixel.replaymod.timer.EnchantmentTimer;
import eu.crushedpixel.replaymod.timer.ReplayTimer;
import eu.crushedpixel.replaymod.video.frame.FrameRenderer;
import eu.crushedpixel.replaymod.video.capturer.RenderInfo;
import eu.crushedpixel.replaymod.video.frame.ARGBFrame;
import eu.crushedpixel.replaymod.video.rendering.Pipeline;
import eu.crushedpixel.replaymod.video.rendering.Pipelines;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.ScaledResolution;
import net.minecraft.util.Timer;
import org.lwjgl.input.Mouse;
import org.lwjgl.opengl.Display;
import org.lwjgl.util.Dimension;
import org.lwjgl.util.ReadableDimension;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.concurrent.FutureTask;
@@ -31,10 +35,8 @@ import static net.minecraft.client.renderer.GlStateManager.*;
import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT;
import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT;
public class VideoRenderer {
public class VideoRenderer implements RenderInfo {
private final Minecraft mc = Minecraft.getMinecraft();
private final FrameRenderer frameRenderer;
private final VideoWriter videoWriter;
private final ReplaySender replaySender;
private final RenderOptions options;
@@ -52,12 +54,19 @@ public class VideoRenderer {
private boolean paused;
private boolean cancelled;
private final Pipeline renderingPipeline;
public VideoRenderer(RenderOptions options) throws IOException {
this.frameRenderer = options.getRenderer();
this.videoWriter = new VideoWriter(this, options);
this.gui = new GuiVideoRenderer(this);
this.replaySender = ReplayMod.replaySender;
this.options = options;
this.renderingPipeline = Pipelines.newPipeline(options.getMode(), this, new VideoWriter(options) {
@Override
public void consume(ARGBFrame frame) {
gui.updatePreview(frame);
super.consume(frame);
}
});
}
/**
@@ -98,33 +107,33 @@ public class VideoRenderer {
mc.renderGlobal.renderEntitiesStartupCounter = 0;
framesDone = 0;
while (framesDone < totalFrames && !cancelled) {
pauseIfNeeded();
if (Display.isActive() && Display.isCloseRequested()) {
mc.shutdown();
return false;
}
updateTime(timer, framesDone);
int elapsedTicks = timer.elapsedTicks;
while (elapsedTicks-- > 0) {
tick();
}
frame(timer);
framesDone++;
drawGui();
System.gc();
}
renderingPipeline.run();
finish();
return !cancelled;
}
@Override
public float updateForNextFrame() {
drawGui();
updateTime(mc.timer, framesDone);
int elapsedTicks = mc.timer.elapsedTicks;
while (elapsedTicks-- > 0) {
tick();
}
updateCam();
framesDone++;
return mc.timer.renderPartialTicks;
}
@Override
public RenderOptions getRenderOptions() {
return options;
}
private void setup() {
if (Mouse.isGrabbed()) {
mouseWasGrabbed = true;
@@ -171,7 +180,6 @@ public class VideoRenderer {
}
time.prepare();
frameRenderer.setup();
ScaledResolution scaled = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
gui.setWorldAndResolution(mc, scaled.getScaledWidth(), scaled.getScaledHeight());
@@ -182,18 +190,8 @@ public class VideoRenderer {
}
private void finish() {
if (cancelled) {
videoWriter.abortRecording();
} else {
videoWriter.endRecording();
}
try {
videoWriter.waitForFinish();
} catch (InterruptedException ignored) { }
frameRenderer.tearDown();
if (mouseWasGrabbed) {
Mouse.setGrabbed(true);
mc.mouseHelper.grabMouseCursor();
}
ReplayTimer.get(mc).passive = false;
mc.displayGuiScreen(null);
@@ -202,6 +200,8 @@ public class VideoRenderer {
}
ReplayMod.soundHandler.playRenderSuccessSound();
mc.displayGuiScreen(null);
}
private void updateCam() {
@@ -320,9 +320,7 @@ public class VideoRenderer {
TimestampValue timestampValue = new TimestampValue();
time.applyPoint(Math.max(0, Math.min(1, timePos)), timestampValue);
Integer replayTime = (int) timestampValue.value;
if(replayTime != null) {
replaySender.sendPacketsTill(replayTime);
}
replaySender.sendPacketsTill(replayTime);
if (curSpeed >= 0) {
replaySender.setReplaySpeed(curSpeed);
@@ -337,7 +335,7 @@ public class VideoRenderer {
timer.renderPartialTicks = timer.elapsedPartialTicks;
}
private void tick() throws IOException {
private void tick() {
synchronized (mc.scheduledTasks) {
while (!mc.scheduledTasks.isEmpty()) {
((FutureTask) mc.scheduledTasks.poll()).run();
@@ -345,89 +343,70 @@ public class VideoRenderer {
}
mc.currentScreen = gui;
mc.runTick();
}
private void frame(Timer timer) {
updateCam();
BufferedImage frame = null;
while (frame == null) {
try {
frame = frameRenderer.captureFrame(timer);
} catch (OutOfMemoryError e) {
System.out.println("Caught oom error-> calling garbage collector, decreasing queue size and waiting for video writer");
System.gc();
videoWriter.waitTillQueueEmpty();
}
}
while (!videoWriter.writeImage(frame, false)) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
return;
}
drawGui();
}
}
private void pauseIfNeeded() {
while (paused && !cancelled && !Display.isCloseRequested()) {
drawGui();
try {
Thread.sleep(20);
} catch (InterruptedException e) {
return;
}
try {
mc.runTick();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void drawGui() {
pushMatrix();
clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
enableTexture2D();
mc.getFramebuffer().bindFramebuffer(true);
mc.entityRenderer.setupOverlayRendering();
do {
pushMatrix();
clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
enableTexture2D();
mc.getFramebuffer().bindFramebuffer(true);
mc.entityRenderer.setupOverlayRendering();
try {
gui.handleInput();
} catch (IOException e) {
// That's a strange exception from this kind of method O_o
// It isn't actually thrown here, so we'll deal with it the easy way
throw new RuntimeException(e);
}
try {
gui.handleInput();
} catch (IOException e) {
// That's a strange exception from this kind of method O_o
// It isn't actually thrown here, so we'll deal with it the easy way
throw new RuntimeException(e);
}
ScaledResolution scaled = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
int mouseX = Mouse.getX() * scaled.getScaledWidth() / mc.displayWidth;
int mouseY = scaled.getScaledHeight() - Mouse.getY() * scaled.getScaledHeight() / mc.displayHeight - 1;
gui.drawScreen(mouseX, mouseY, 0);
ScaledResolution scaled = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
int mouseX = Mouse.getX() * scaled.getScaledWidth() / mc.displayWidth;
int mouseY = scaled.getScaledHeight() - Mouse.getY() * scaled.getScaledHeight() / mc.displayHeight - 1;
gui.drawScreen(mouseX, mouseY, 0);
mc.getFramebuffer().unbindFramebuffer();
popMatrix();
pushMatrix();
mc.getFramebuffer().framebufferRender(mc.displayWidth, mc.displayHeight);
popMatrix();
mc.getFramebuffer().unbindFramebuffer();
popMatrix();
pushMatrix();
mc.getFramebuffer().framebufferRender(mc.displayWidth, mc.displayHeight);
popMatrix();
Display.update();
if (Mouse.isGrabbed()) {
Mouse.setGrabbed(false);
}
Display.update();
if (Mouse.isGrabbed()) {
Mouse.setGrabbed(false);
}
if (paused) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
} while (paused);
}
public int getFramesDone() {
return framesDone;
}
@Override
public ReadableDimension getFrameSize() {
return new Dimension(options.getWidth(), options.getHeight());
}
public int getTotalFrames() {
return totalFrames;
}
public int getVideoTime() { return framesDone * 1000 / fps; }
public FrameRenderer getFrameRenderer() {
return frameRenderer;
}
public void setPaused(boolean paused) {
this.paused = paused;
}
@@ -438,5 +417,6 @@ public class VideoRenderer {
public void cancel() {
this.cancelled = true;
renderingPipeline.cancel();
}
}

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);
}
}

View File

@@ -0,0 +1,4 @@
package eu.crushedpixel.replaymod.video.capturer;
public interface CaptureData {
}

View File

@@ -0,0 +1,37 @@
package eu.crushedpixel.replaymod.video.capturer;
import eu.crushedpixel.replaymod.video.frame.CubicOpenGlFrame;
public class CubicOpenGlFrameCapturer extends OpenGlFrameCapturer<CubicOpenGlFrame, CubicOpenGlFrameCapturer.Data> {
public enum Data implements CaptureData {
LEFT, RIGHT, FRONT, BACK, TOP, BOTTOM
}
private final int frameSize;
public CubicOpenGlFrameCapturer(WorldRenderer<Data> worldRenderer, RenderInfo renderInfo, int frameSize) {
super(worldRenderer, renderInfo);
this.frameSize = frameSize;
}
@Override
protected int getFrameWidth() {
return frameSize;
}
@Override
protected int getFrameHeight() {
return frameSize;
}
@Override
public CubicOpenGlFrame process() {
float partialTicks = renderInfo.updateForNextFrame();
int frameId = framesDone++;
return new CubicOpenGlFrame(renderFrame(frameId, partialTicks, Data.LEFT),
renderFrame(frameId, partialTicks, Data.RIGHT),
renderFrame(frameId, partialTicks, Data.FRONT),
renderFrame(frameId, partialTicks, Data.BACK),
renderFrame(frameId, partialTicks, Data.TOP),
renderFrame(frameId, partialTicks, Data.BOTTOM));
}
}

View File

@@ -0,0 +1,108 @@
package eu.crushedpixel.replaymod.video.capturer;
import eu.crushedpixel.replaymod.utils.ByteBufferPool;
import eu.crushedpixel.replaymod.video.frame.OpenGlFrame;
import eu.crushedpixel.replaymod.video.rendering.Frame;
import eu.crushedpixel.replaymod.video.rendering.FrameCapturer;
import net.minecraft.client.renderer.OpenGlHelper;
import net.minecraft.client.shader.Framebuffer;
import org.lwjgl.opengl.GL11;
import org.lwjgl.util.Dimension;
import org.lwjgl.util.ReadableDimension;
import org.lwjgl.util.WritableDimension;
import java.io.IOException;
import java.nio.ByteBuffer;
import static net.minecraft.client.renderer.GlStateManager.*;
import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT;
import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT;
public abstract class OpenGlFrameCapturer<F extends Frame, D extends CaptureData> implements FrameCapturer<F> {
protected final WorldRenderer<D> worldRenderer;
protected final RenderInfo renderInfo;
protected int framesDone;
private Framebuffer frameBuffer;
public OpenGlFrameCapturer(WorldRenderer<D> worldRenderer, RenderInfo renderInfo) {
this.worldRenderer = worldRenderer;
this.renderInfo = renderInfo;
}
protected final ReadableDimension frameSize = new ReadableDimension() {
@Override
public int getWidth() {
return getFrameWidth();
}
@Override
public int getHeight() {
return getFrameHeight();
}
@Override
public void getSize(WritableDimension dest) {
dest.setSize(getWidth(), getHeight());
}
};
protected int getFrameWidth() {
return renderInfo.getFrameSize().getWidth();
}
protected int getFrameHeight() {
return renderInfo.getFrameSize().getHeight();
}
private Framebuffer frameBuffer() {
if (frameBuffer == null) {
frameBuffer = new Framebuffer(getFrameWidth(), getFrameHeight(), true);
}
return frameBuffer;
}
@Override
public boolean isDone() {
return framesDone >= renderInfo.getTotalFrames();
}
protected OpenGlFrame renderFrame(int frameId, float partialTicks) {
return renderFrame(frameId, partialTicks, null);
}
protected OpenGlFrame renderFrame(int frameId, float partialTicks, D captureData) {
pushMatrix();
frameBuffer().bindFramebuffer(true);
clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
enableTexture2D();
worldRenderer.renderWorld(frameSize, partialTicks, captureData);
frameBuffer().unbindFramebuffer();
popMatrix();
GL11.glPixelStorei(GL11.GL_PACK_ALIGNMENT, 1);
GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1);
ByteBuffer buffer = ByteBufferPool.allocate(getFrameWidth() * getFrameHeight() * 3);
if (OpenGlHelper.isFramebufferEnabled()) {
frameBuffer().bindFramebufferTexture();
GL11.glGetTexImage(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGB, GL11.GL_UNSIGNED_BYTE, buffer);
frameBuffer().unbindFramebufferTexture();
} else {
GL11.glReadPixels(0, 0, getFrameWidth(), getFrameHeight(), GL11.GL_RGB, GL11.GL_UNSIGNED_BYTE, buffer);
}
buffer.rewind();
return new OpenGlFrame(frameId, new Dimension(getFrameWidth(), getFrameHeight()), buffer);
}
@Override
public void close() throws IOException {
worldRenderer.close();
if (frameBuffer != null) {
frameBuffer.deleteFramebuffer();
}
}
}

View File

@@ -0,0 +1,14 @@
package eu.crushedpixel.replaymod.video.capturer;
import eu.crushedpixel.replaymod.settings.RenderOptions;
import org.lwjgl.util.ReadableDimension;
public interface RenderInfo {
ReadableDimension getFrameSize();
int getTotalFrames();
float updateForNextFrame();
RenderOptions getRenderOptions();
}

View File

@@ -0,0 +1,16 @@
package eu.crushedpixel.replaymod.video.capturer;
import eu.crushedpixel.replaymod.video.frame.OpenGlFrame;
public class SimpleOpenGlFrameCapturer extends OpenGlFrameCapturer<OpenGlFrame, CaptureData> {
public SimpleOpenGlFrameCapturer(WorldRenderer<CaptureData> worldRenderer, RenderInfo renderInfo) {
super(worldRenderer, renderInfo);
}
@Override
public OpenGlFrame process() {
float partialTicks = renderInfo.updateForNextFrame();
return renderFrame(framesDone++, partialTicks);
}
}

View File

@@ -0,0 +1,29 @@
package eu.crushedpixel.replaymod.video.capturer;
import eu.crushedpixel.replaymod.video.frame.OpenGlFrame;
import eu.crushedpixel.replaymod.video.frame.StereoscopicOpenGlFrame;
public class StereoscopicOpenGlFrameCapturer
extends OpenGlFrameCapturer<StereoscopicOpenGlFrame, StereoscopicOpenGlFrameCapturer.Data> {
public enum Data implements CaptureData {
LEFT_EYE, RIGHT_EYE
}
public StereoscopicOpenGlFrameCapturer(WorldRenderer<Data> worldRenderer, RenderInfo renderInfo) {
super(worldRenderer, renderInfo);
}
@Override
protected int getFrameWidth() {
return super.getFrameWidth() / 2;
}
@Override
public StereoscopicOpenGlFrame process() {
float partialTicks = renderInfo.updateForNextFrame();
int frameId = framesDone++;
OpenGlFrame left = renderFrame(frameId, partialTicks, Data.LEFT_EYE);
OpenGlFrame right = renderFrame(frameId, partialTicks, Data.RIGHT_EYE);
return new StereoscopicOpenGlFrame(left, right);
}
}

View File

@@ -0,0 +1,9 @@
package eu.crushedpixel.replaymod.video.capturer;
import org.lwjgl.util.ReadableDimension;
import java.io.Closeable;
public interface WorldRenderer<D extends CaptureData> extends Closeable {
void renderWorld(ReadableDimension displaySize, float partialTicks, D data);
}

View File

@@ -1,7 +1,7 @@
package eu.crushedpixel.replaymod.video.entity;
import eu.crushedpixel.replaymod.replay.ReplayHandler;
import eu.crushedpixel.replaymod.settings.RenderOptions;
import eu.crushedpixel.replaymod.video.capturer.CubicOpenGlFrameCapturer;
import net.minecraft.client.particle.EntityFX;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.client.renderer.WorldRenderer;
@@ -9,39 +9,13 @@ import net.minecraft.client.renderer.entity.RenderManager;
import net.minecraft.entity.Entity;
import net.minecraft.util.ResourceLocation;
import java.io.IOException;
import java.lang.reflect.Field;
public class CubicEntityRenderer extends CustomEntityRenderer {
public class CubicEntityRenderer extends CustomEntityRenderer<CubicOpenGlFrameCapturer.Data> {
public enum Direction {
TOP(1), BOTTOM(9), LEFT(4), FRONT(5), RIGHT(6), BACK(7);
private final int frame;
Direction(int frame) {
this.frame = frame;
}
public static Direction forFrame(int frame) {
for (Direction d : values()) {
if (d.frame == frame) {
return d;
}
}
return null;
}
public int getCubicFrame() {
return frame;
}
}
private final boolean stable;
private Direction direction;
public CubicEntityRenderer(RenderOptions options, int frameSize, boolean stable) {
super(options, frameSize, frameSize);
this.stable = stable;
public CubicEntityRenderer(RenderOptions options) {
super(options);
try {
Field hookField = RenderManager.class.getField("hook");
@@ -54,8 +28,8 @@ public class CubicEntityRenderer extends CustomEntityRenderer {
}
@Override
public void cleanup() {
super.cleanup();
public void close() throws IOException{
super.close();
try {
Field hookField = RenderManager.class.getField("hook");
hookField.set(mc.getRenderManager(), null);
@@ -66,14 +40,6 @@ public class CubicEntityRenderer extends CustomEntityRenderer {
}
}
public void setFrame(int id) {
setDirection(Direction.forFrame(id));
}
public void setDirection(Direction direction) {
this.direction = direction;
}
@Override
protected void gluPerspective(float fovY, float aspect, float zNear, float zFar) {
super.gluPerspective(90, 1, zNear, zFar);
@@ -88,32 +54,10 @@ public class CubicEntityRenderer extends CustomEntityRenderer {
proxied.useShader = false;
}
@Override
protected void setupCameraTransform(float partialTicks) {
Entity entity = mc.getRenderViewEntity();
float orgYaw = entity.rotationYaw;
float orgPitch = entity.rotationPitch;
float orgPrevYaw = entity.prevRotationYaw;
float orgPrevPitch = entity.prevRotationPitch;
float orgRoll = ReplayHandler.getCameraTilt();
super.setupCameraTransform(partialTicks);
entity.rotationYaw = orgYaw;
entity.rotationPitch = orgPitch;
entity.prevRotationYaw = orgPrevYaw;
entity.prevRotationPitch = orgPrevPitch;
if (stable) {
ReplayHandler.setCameraTilt(orgRoll);
}
}
@Override
protected void orientCamera(float partialTicks) {
Entity entity = mc.getRenderViewEntity();
// Rotate
switch(direction) {
switch (data) {
case FRONT:
GlStateManager.rotate(0, 0.0F, 1.0F, 0.0F);
break;
@@ -134,13 +78,6 @@ public class CubicEntityRenderer extends CustomEntityRenderer {
break;
}
if (stable) {
// Stop the minecraft code from doing any rotation
entity.prevRotationPitch = entity.rotationPitch = 0;
entity.prevRotationYaw = entity.rotationYaw = 0;
ReplayHandler.setCameraTilt(0);
}
// Minecraft goes back a little so we have to invert that
GlStateManager.translate(0.0F, 0.0F, 0.1F);
super.orientCamera(partialTicks);

View File

@@ -3,8 +3,7 @@ package eu.crushedpixel.replaymod.video.entity;
import eu.crushedpixel.replaymod.renderer.SpectatorRenderer;
import eu.crushedpixel.replaymod.replay.ReplayHandler;
import eu.crushedpixel.replaymod.settings.RenderOptions;
import eu.crushedpixel.replaymod.utils.OpenGLUtils;
import eu.crushedpixel.replaymod.video.entity.strategy.*;
import eu.crushedpixel.replaymod.video.capturer.CaptureData;
import net.minecraft.client.Minecraft;
import net.minecraft.client.particle.EffectRenderer;
import net.minecraft.client.particle.EntityFX;
@@ -19,16 +18,17 @@ import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.EnumWorldBlockLayer;
import net.minecraft.util.MathHelper;
import net.minecraft.util.ResourceLocation;
import org.lwjgl.util.ReadableDimension;
import org.lwjgl.util.glu.Project;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.List;
import static net.minecraft.client.renderer.GlStateManager.*;
import static org.lwjgl.opengl.GL11.*;
public abstract class CustomEntityRenderer {
public class CustomEntityRenderer<D extends CaptureData> implements eu.crushedpixel.replaymod.video.capturer.WorldRenderer<D> {
public final Minecraft mc = Minecraft.getMinecraft();
public final EntityRenderer proxied = mc.entityRenderer;
protected final SpectatorRenderer spectatorRenderer = new SpectatorRenderer(){
@@ -37,43 +37,18 @@ public abstract class CustomEntityRenderer {
CustomEntityRenderer.this.gluPerspective(fovY, aspect, zNear, zFar);
}
};
protected final FrameRenderingStrategy renderingStrategy;
public GluPerspectiveHook gluPerspectiveHook;
public LoadShaderHook loadShaderHook;
protected int frameCount;
protected D data;
protected final RenderOptions options;
public final int resultWidth;
public final int resultHeight;
public CustomEntityRenderer(RenderOptions options) {
this(options, options.getWidth(), options.getHeight());
}
public CustomEntityRenderer(RenderOptions options, int resultWidth, int resultHeight) {
this.options = options;
this.resultWidth = resultWidth;
this.resultHeight = resultHeight;
if (OpenGlHelper.isFramebufferEnabled()) {
if (resultWidth > OpenGLUtils.VIEWPORT_MAX_WIDTH || resultHeight > OpenGLUtils.VIEWPORT_MAX_HEIGHT) {
// Video resolution only limited by available RAM and disk size
renderingStrategy = new TiledFrameBufferRenderingStrategy(this);
} else {
// This strategy supports shader
renderingStrategy = new VanillaFrameBufferRenderingStrategy(this);
}
} else {
if (resultWidth > mc.displayHeight || resultHeight > mc.displayHeight) {
// Video resolution only limited by available RAM and disk size
renderingStrategy = new TiledReadPixelsRenderingStrategy(this);
} else {
// Simplest and fastest strategy if frame buffers aren't supported
renderingStrategy = new VanillaReadPixelsRenderingStrategy(this);
}
}
System.out.println("CustomEntityRenderer using " + renderingStrategy);
// Install entity renderer hooks
try {
@@ -108,12 +83,6 @@ public abstract class CustomEntityRenderer {
}
}
public void renderFrame(float partialTicks, BufferedImage into, int x, int y) {
if (mc.theWorld != null) {
renderingStrategy.renderFrame(partialTicks, into, x, y);
}
}
public void withDisplaySize(int displayWidth, int displayHeight, Runnable runnable) {
final int prevWidth = mc.displayWidth;
final int prevHeight = mc.displayHeight;
@@ -366,6 +335,13 @@ public abstract class CustomEntityRenderer {
}
protected void setupCameraTransform(float partialTicks) {
Entity entity = mc.getRenderViewEntity();
float orgYaw = entity.rotationYaw;
float orgPitch = entity.rotationPitch;
float orgPrevYaw = entity.prevRotationYaw;
float orgPrevPitch = entity.prevRotationPitch;
float orgRoll = ReplayHandler.getCameraTilt();
proxied.farPlaneDistance = (float)(this.mc.gameSettings.renderDistanceChunks * 16);
matrixMode(GL_PROJECTION);
@@ -377,6 +353,12 @@ public abstract class CustomEntityRenderer {
loadIdentity();
orientCamera(partialTicks);
entity.rotationYaw = orgYaw;
entity.rotationPitch = orgPitch;
entity.prevRotationYaw = orgPrevYaw;
entity.prevRotationPitch = orgPrevPitch;
ReplayHandler.setCameraTilt(orgRoll);
}
protected void setupFog(int fogDistanceFlag, float partialTicks) {
@@ -386,6 +368,14 @@ public abstract class CustomEntityRenderer {
}
protected void orientCamera(float partialTicks) {
if (options.isIgnoreCameraRotation()) {
Entity entity = mc.getRenderViewEntity();
// Stop the minecraft code from doing any rotation
entity.prevRotationPitch = entity.rotationPitch = 0;
entity.prevRotationYaw = entity.rotationYaw = 0;
ReplayHandler.setCameraTilt(0);
}
proxied.orientCamera(partialTicks);
}
@@ -407,7 +397,8 @@ public abstract class CustomEntityRenderer {
}
}
public void cleanup() {
@Override
public void close() throws IOException {
try {
Field hookField = EntityRenderer.class.getField("hook");
hookField.set(proxied, null);
@@ -417,10 +408,30 @@ public abstract class CustomEntityRenderer {
throw new Error(e);
}
renderingStrategy.cleanup();
spectatorRenderer.cleanup();
}
@Override
public void renderWorld(ReadableDimension displaySize, final float partialTicks, D data) {
this.data = data;
withDisplaySize(displaySize.getWidth(), displaySize.getHeight(), new Runnable() {
@Override
public void run() {
renderWorld(partialTicks, 0);
if (OpenGlHelper.shadersSupported) {
if (proxied.theShaderGroup != null && proxied.useShader) {
matrixMode(GL_TEXTURE);
pushMatrix();
loadIdentity();
proxied.theShaderGroup.loadShaderGroup(partialTicks);
popMatrix();
}
}
}
});
}
public static final class NoCullingCamera implements ICamera {
@Override

View File

@@ -1,32 +1,20 @@
package eu.crushedpixel.replaymod.video.entity;
import eu.crushedpixel.replaymod.settings.RenderOptions;
import eu.crushedpixel.replaymod.video.capturer.StereoscopicOpenGlFrameCapturer;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.util.MathHelper;
import net.minecraft.util.ResourceLocation;
import java.awt.image.BufferedImage;
import static net.minecraft.client.renderer.GlStateManager.loadIdentity;
import static net.minecraft.client.renderer.GlStateManager.matrixMode;
import static org.lwjgl.opengl.GL11.GL_MODELVIEW;
import static org.lwjgl.opengl.GL11.GL_PROJECTION;
public class StereoscopicEntityRenderer extends CustomEntityRenderer {
private boolean leftEye;
public class StereoscopicEntityRenderer extends CustomEntityRenderer<StereoscopicOpenGlFrameCapturer.Data> {
public StereoscopicEntityRenderer(RenderOptions options) {
super(options, options.getWidth() / 2, options.getHeight());
}
public void setEye(boolean leftEye) {
this.leftEye = leftEye;
}
@Override
public void renderFrame(float partialTicks, BufferedImage into, int x, int y) {
super.renderFrame(partialTicks, into, x, y);
super(options);
}
@Override
@@ -39,7 +27,7 @@ public class StereoscopicEntityRenderer extends CustomEntityRenderer {
}
protected void translateStereoscopic() {
GlStateManager.translate(leftEye ? 0.07 : -0.07, 0, 0);
GlStateManager.translate(data == StereoscopicOpenGlFrameCapturer.Data.LEFT_EYE ? 0.07 : -0.07, 0, 0);
}
@Override
@@ -61,6 +49,6 @@ public class StereoscopicEntityRenderer extends CustomEntityRenderer {
@Override
protected void renderSpectatorHand(float partialTicks, int renderPass) {
super.renderSpectatorHand(partialTicks, leftEye ? 1 : 0);
super.renderSpectatorHand(partialTicks, data == StereoscopicOpenGlFrameCapturer.Data.LEFT_EYE ? 1 : 0);
}
}

View File

@@ -1,8 +0,0 @@
package eu.crushedpixel.replaymod.video.entity.strategy;
import java.awt.image.BufferedImage;
public interface FrameRenderingStrategy {
void renderFrame(float partialTicks, BufferedImage into, int x, int y);
void cleanup();
}

View File

@@ -1,127 +0,0 @@
package eu.crushedpixel.replaymod.video.entity.strategy;
import eu.crushedpixel.replaymod.utils.OpenGLUtils;
import eu.crushedpixel.replaymod.video.entity.CustomEntityRenderer;
import net.minecraft.client.shader.Framebuffer;
import net.minecraft.crash.CrashReport;
import net.minecraft.util.ReportedException;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import java.awt.image.BufferedImage;
import java.nio.ByteBuffer;
import static net.minecraft.client.renderer.GlStateManager.*;
import static org.lwjgl.opengl.GL11.*;
public class TiledFrameBufferRenderingStrategy implements FrameRenderingStrategy, CustomEntityRenderer.GluPerspectiveHook {
private final CustomEntityRenderer renderer;
private final int tileWidth;
private final int tileHeight;
private final ByteBuffer buffer;
private Framebuffer frameBuffer;
private int tileX;
private int tileY;
public TiledFrameBufferRenderingStrategy(CustomEntityRenderer renderer) {
this.renderer = renderer;
this.tileWidth = OpenGLUtils.VIEWPORT_MAX_WIDTH;
this.tileHeight = OpenGLUtils.VIEWPORT_MAX_HEIGHT;
this.buffer = BufferUtils.createByteBuffer(tileWidth * tileHeight * 3);
renderer.gluPerspectiveHook = this;
}
@Override
public void gluPerspective(float fovY, float aspect, float zNear, float zFar) {
double height = Math.tan(fovY / 360 * Math.PI) * zNear;
double width = height * aspect;
double tilesX = (double) renderer.resultWidth / tileWidth;
double tilesY = (double) renderer.resultHeight / tileHeight;
double tilesMin = Math.min(tilesX, tilesY);
scale(tilesMin, tilesMin, 1);
double xOffset = (2 * tileX - tilesX + 1) / tilesMin;
double yOffset = (2 * tileY - tilesY + 1) / tilesMin;
translate(-xOffset, yOffset, 0);
glFrustum(-width, width, -height, height, zNear, zFar);
}
private Framebuffer frameBuffer() {
if (frameBuffer == null) {
frameBuffer = new Framebuffer(tileWidth, tileHeight, true);
}
return frameBuffer;
}
@Override
public void renderFrame(float partialTicks, BufferedImage into, int x, int y) {
int tilesX = getTilesX();
int tilesY = getTilesY();
for (tileX = 0; tileX < tilesX; tileX++) {
for (tileY = 0; tileY < tilesY; tileY++) {
try {
renderTile(partialTicks, into, x + tileWidth * tileX, y + tileHeight * tileY);
} catch (Throwable t) {
CrashReport crash = CrashReport.makeCrashReport(t, "Rendering tile " + x + "/" + y);
throw new ReportedException(crash);
}
}
}
}
public void renderTile(final float partialTicks, BufferedImage into, int x, int y) {
pushMatrix();
frameBuffer().bindFramebuffer(true);
clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
enableTexture2D();
renderer.withDisplaySize(tileWidth, tileHeight, new Runnable() {
@Override
public void run() {
renderer.renderWorld(partialTicks, 0);
}
});
frameBuffer().unbindFramebuffer();
popMatrix();
GL11.glPixelStorei(GL11.GL_PACK_ALIGNMENT, 1);
GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1);
buffer.clear();
frameBuffer().bindFramebufferTexture();
GL11.glGetTexImage(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGB, GL11.GL_UNSIGNED_BYTE, buffer);
frameBuffer().unbindFramebufferTexture();
OpenGLUtils.openGlBytesToBufferedImage(buffer, tileWidth, into, x, y);
}
public int getTilesX() {
int videoWidth = renderer.resultWidth;
if (videoWidth % tileWidth == 0) {
return videoWidth / tileWidth;
} else {
return videoWidth / tileWidth + 1;
}
}
public int getTilesY() {
int videoHeight = renderer.resultHeight;
if (videoHeight % tileHeight == 0) {
return videoHeight / tileHeight;
} else {
return videoHeight / tileHeight + 1;
}
}
@Override
public void cleanup() {
if (frameBuffer != null) {
frameBuffer.deleteFramebuffer();
}
}
}

View File

@@ -1,112 +0,0 @@
package eu.crushedpixel.replaymod.video.entity.strategy;
import eu.crushedpixel.replaymod.utils.OpenGLUtils;
import eu.crushedpixel.replaymod.video.entity.CustomEntityRenderer;
import net.minecraft.crash.CrashReport;
import net.minecraft.util.ReportedException;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import java.awt.image.BufferedImage;
import java.nio.ByteBuffer;
import static net.minecraft.client.renderer.GlStateManager.*;
import static org.lwjgl.opengl.GL11.*;
public class TiledReadPixelsRenderingStrategy implements FrameRenderingStrategy, CustomEntityRenderer.GluPerspectiveHook {
private final CustomEntityRenderer renderer;
private final int tileWidth;
private final int tileHeight;
private final ByteBuffer buffer;
private int tileX;
private int tileY;
public TiledReadPixelsRenderingStrategy(CustomEntityRenderer renderer) {
this.renderer = renderer;
this.tileWidth = renderer.mc.displayWidth;
this.tileHeight = renderer.mc.displayHeight;
this.buffer = BufferUtils.createByteBuffer(tileWidth * tileHeight * 3);
renderer.gluPerspectiveHook = this;
}
@Override
public void gluPerspective(float fovY, float aspect, float zNear, float zFar) {
double height = Math.tan(fovY / 360 * Math.PI) * zNear;
double width = height * aspect;
double tilesX = (double) renderer.resultWidth / tileWidth;
double tilesY = (double) renderer.resultHeight / tileHeight;
double tilesMin = Math.min(tilesX, tilesY);
scale(tilesMin, tilesMin, 1);
double xOffset = (2 * tileX - tilesX + 1) / tilesMin;
double yOffset = (2 * tileY - tilesY + 1) / tilesMin;
translate(-xOffset, yOffset, 0);
glFrustum(-width, width, -height, height, zNear, zFar);
}
@Override
public void renderFrame(float partialTicks, BufferedImage into, int x, int y) {
int tilesX = getTilesX();
int tilesY = getTilesY();
for (tileX = 0; tileX < tilesX; tileX++) {
for (tileY = 0; tileY < tilesY; tileY++) {
try {
renderTile(partialTicks, into, x + tileWidth * tileX, y + tileHeight * tileY);
} catch (Throwable t) {
CrashReport crash = CrashReport.makeCrashReport(t, "Rendering tile " + x + "/" + y);
throw new ReportedException(crash);
}
}
}
}
public void renderTile(final float partialTicks, BufferedImage into, int x, int y) {
pushMatrix();
clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
enableTexture2D();
renderer.withDisplaySize(tileWidth, tileHeight, new Runnable() {
@Override
public void run() {
renderer.renderWorld(partialTicks, 0);
}
});
popMatrix();
buffer.clear();
GL11.glPixelStorei(GL11.GL_PACK_ALIGNMENT, 1);
GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1);
GL11.glReadPixels(0, 0, tileWidth, tileHeight, GL11.GL_RGB, GL11.GL_UNSIGNED_BYTE, buffer);
buffer.rewind();
OpenGLUtils.openGlBytesToBufferedImage(buffer, tileWidth, into, x, y);
}
public int getTilesX() {
int videoWidth = renderer.resultWidth;
if (videoWidth % tileWidth == 0) {
return videoWidth / tileWidth;
} else {
return videoWidth / tileWidth + 1;
}
}
public int getTilesY() {
int videoHeight = renderer.resultHeight;
if (videoHeight % tileHeight == 0) {
return videoHeight / tileHeight;
} else {
return videoHeight / tileHeight + 1;
}
}
@Override
public void cleanup() {
}
}

View File

@@ -1,112 +0,0 @@
package eu.crushedpixel.replaymod.video.entity.strategy;
import com.google.gson.JsonSyntaxException;
import eu.crushedpixel.replaymod.utils.OpenGLUtils;
import eu.crushedpixel.replaymod.video.entity.CustomEntityRenderer;
import net.minecraft.client.renderer.EntityRenderer;
import net.minecraft.client.renderer.OpenGlHelper;
import net.minecraft.client.shader.Framebuffer;
import net.minecraft.client.shader.ShaderGroup;
import net.minecraft.util.ResourceLocation;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.ByteBuffer;
import static net.minecraft.client.renderer.GlStateManager.*;
import static org.lwjgl.opengl.GL11.*;
public class VanillaFrameBufferRenderingStrategy implements FrameRenderingStrategy, CustomEntityRenderer.LoadShaderHook {
private static final Logger logger = LogManager.getLogger();
private final CustomEntityRenderer renderer;
private final ByteBuffer buffer;
private Framebuffer frameBuffer;
public VanillaFrameBufferRenderingStrategy(CustomEntityRenderer renderer) {
this.renderer = renderer;
this.buffer = BufferUtils.createByteBuffer(renderer.resultWidth * renderer.resultHeight * 3);
}
@Override
public void loadShader(ResourceLocation resourceLocation) {
EntityRenderer proxied = renderer.proxied;
try {
ShaderGroup theShaderGroup = new ShaderGroup(renderer.mc.getTextureManager(), proxied.resourceManager, frameBuffer(), resourceLocation);
theShaderGroup.createBindFramebuffers(renderer.resultWidth, renderer.resultHeight);
proxied.theShaderGroup = theShaderGroup;
proxied.useShader = true;
} catch (IOException e) {
logger.warn("Failed to load shader: " + resourceLocation, e);
proxied.useShader = false;
} catch (JsonSyntaxException e) {
logger.warn("Failed to load shader: " + resourceLocation, e);
proxied.useShader = false;
}
}
private Framebuffer frameBuffer() {
if (frameBuffer == null) {
frameBuffer = new Framebuffer(renderer.resultWidth, renderer.resultHeight, true);
renderer.loadShaderHook = this;
renderer.proxied.loadEntityShader(renderer.mc.getRenderViewEntity());
}
return frameBuffer;
}
@Override
public void renderFrame(final float partialTicks, BufferedImage into, int x, int y) {
pushMatrix();
frameBuffer().bindFramebuffer(true);
clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
enableTexture2D();
renderer.withDisplaySize(renderer.resultWidth, renderer.resultHeight, new Runnable() {
@Override
public void run() {
renderer.renderWorld(partialTicks, 0);
if (OpenGlHelper.shadersSupported) {
EntityRenderer proxied = renderer.proxied;
if (proxied.theShaderGroup != null && proxied.useShader) {
matrixMode(GL_TEXTURE);
pushMatrix();
loadIdentity();
proxied.theShaderGroup.loadShaderGroup(partialTicks);
popMatrix();
}
}
}
});
frameBuffer().unbindFramebuffer();
popMatrix();
GL11.glPixelStorei(GL11.GL_PACK_ALIGNMENT, 1);
GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1);
buffer.clear();
frameBuffer().bindFramebufferTexture();
GL11.glGetTexImage(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGB, GL11.GL_UNSIGNED_BYTE, buffer);
frameBuffer().unbindFramebufferTexture();
OpenGLUtils.openGlBytesToBufferedImage(buffer, renderer.resultWidth, into, x, y);
}
@Override
public void cleanup() {
if (frameBuffer != null) {
frameBuffer.deleteFramebuffer();
}
if (renderer.proxied.theShaderGroup != null) {
renderer.proxied.theShaderGroup.createBindFramebuffers(renderer.mc.displayWidth, renderer.mc.displayHeight);
}
}
}

View File

@@ -1,54 +0,0 @@
package eu.crushedpixel.replaymod.video.entity.strategy;
import eu.crushedpixel.replaymod.utils.OpenGLUtils;
import eu.crushedpixel.replaymod.video.entity.CustomEntityRenderer;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import java.awt.image.BufferedImage;
import java.nio.ByteBuffer;
import static net.minecraft.client.renderer.GlStateManager.*;
import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT;
import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT;
public class VanillaReadPixelsRenderingStrategy implements FrameRenderingStrategy {
private final CustomEntityRenderer renderer;
private final ByteBuffer buffer;
public VanillaReadPixelsRenderingStrategy(CustomEntityRenderer renderer) {
this.renderer = renderer;
this.buffer = BufferUtils.createByteBuffer(renderer.resultWidth * renderer.resultHeight * 3);
}
@Override
public void renderFrame(final float partialTicks, BufferedImage into, int x, int y) {
pushMatrix();
clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
enableTexture2D();
renderer.withDisplaySize(renderer.resultWidth, renderer.resultHeight, new Runnable() {
@Override
public void run() {
renderer.renderWorld(partialTicks, 0);
}
});
popMatrix();
buffer.clear();
GL11.glPixelStorei(GL11.GL_PACK_ALIGNMENT, 1);
GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1);
GL11.glReadPixels(0, 0, renderer.resultWidth, renderer.resultHeight, GL11.GL_RGB, GL11.GL_UNSIGNED_BYTE, buffer);
buffer.rewind();
OpenGLUtils.openGlBytesToBufferedImage(buffer, renderer.resultWidth, into, x, y);
}
@Override
public void cleanup() {
}
}

View File

@@ -0,0 +1,28 @@
package eu.crushedpixel.replaymod.video.frame;
import eu.crushedpixel.replaymod.video.rendering.Frame;
import lombok.Getter;
import org.apache.commons.lang3.Validate;
import org.lwjgl.util.ReadableDimension;
import java.nio.ByteBuffer;
public class ARGBFrame implements Frame {
@Getter
private final int frameId;
@Getter
private final ReadableDimension size;
@Getter
private final ByteBuffer byteBuffer;
public ARGBFrame(int frameId, ReadableDimension size, ByteBuffer byteBuffer) {
Validate.isTrue(size.getWidth() * size.getHeight() * 4 == byteBuffer.remaining(),
"Buffer size is %d (cap: %d) but should be %d",
byteBuffer.remaining(), byteBuffer.capacity(), size.getWidth() * size.getHeight() * 4);
this.frameId = frameId;
this.size = size;
this.byteBuffer = byteBuffer;
}
}

View File

@@ -1,46 +0,0 @@
package eu.crushedpixel.replaymod.video.frame;
import eu.crushedpixel.replaymod.settings.RenderOptions;
import eu.crushedpixel.replaymod.video.entity.CubicEntityRenderer;
import net.minecraft.crash.CrashReport;
import net.minecraft.util.ReportedException;
import net.minecraft.util.Timer;
import java.awt.image.BufferedImage;
public class CubicFrameRenderer extends FrameRenderer {
protected final int frameSize;
protected final CubicEntityRenderer entityRenderer;
protected CubicFrameRenderer(RenderOptions options, int frameSize, boolean stable) {
super(options);
this.frameSize = frameSize;
this.entityRenderer = new CubicEntityRenderer(options, frameSize, stable);
setCustomEntityRenderer(entityRenderer);
}
public CubicFrameRenderer(RenderOptions options, boolean stable) {
this(options, options.getWidth() / 4, stable);
}
@Override
public BufferedImage captureFrame(Timer timer) {
BufferedImage image = new BufferedImage(getVideoWidth(), getVideoHeight(), BufferedImage.TYPE_INT_RGB);
for (CubicEntityRenderer.Direction direction : CubicEntityRenderer.Direction.values()) {
try {
entityRenderer.setDirection(direction);
int frame = direction.getCubicFrame();
int xVideo = frame % 4 * frameSize;
int yVideo = frame / 4 * frameSize;
renderFrame(timer, image, xVideo, yVideo);
} catch (Throwable t) {
CrashReport crash = CrashReport.makeCrashReport(t, "Rendering frame " + direction + ".");
throw new ReportedException(crash);
}
}
updateDefaultPreview(image);
return image;
}
}

View File

@@ -0,0 +1,34 @@
package eu.crushedpixel.replaymod.video.frame;
import eu.crushedpixel.replaymod.video.rendering.Frame;
import lombok.Getter;
import org.apache.commons.lang3.Validate;
public class CubicOpenGlFrame implements Frame {
@Getter
private final OpenGlFrame left, right, front, back, top, bottom;
public CubicOpenGlFrame(OpenGlFrame left, OpenGlFrame right, OpenGlFrame front, OpenGlFrame back, OpenGlFrame top, OpenGlFrame bottom) {
Validate.isTrue(left.getFrameId() == right.getFrameId()
&& right.getFrameId() == front.getFrameId()
&& front.getFrameId() == back.getFrameId()
&& back.getFrameId() == top.getFrameId()
&& top.getFrameId() == bottom.getFrameId(), "Frame ids do not match.");
Validate.isTrue(left.getByteBuffer().remaining() == right.getByteBuffer().remaining()
&& right.getByteBuffer().remaining() == front.getByteBuffer().remaining()
&& front.getByteBuffer().remaining() == back.getByteBuffer().remaining()
&& back.getByteBuffer().remaining() == top.getByteBuffer().remaining()
&& top.getByteBuffer().remaining() == bottom.getByteBuffer().remaining(), "Buffer size does not match.");
this.left = left;
this.right = right;
this.front = front;
this.back = back;
this.top = top;
this.bottom = bottom;
}
@Override
public int getFrameId() {
return left.getFrameId();
}
}

View File

@@ -1,24 +0,0 @@
package eu.crushedpixel.replaymod.video.frame;
import eu.crushedpixel.replaymod.settings.RenderOptions;
import net.minecraft.util.Timer;
import java.awt.image.BufferedImage;
public class DefaultFrameRenderer extends FrameRenderer {
public DefaultFrameRenderer(RenderOptions options) {
super(options);
}
@Override
public BufferedImage captureFrame(Timer timer) {
int width = getVideoWidth();
int height = getVideoHeight();
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
renderFrame(timer, image, 0, 0);
updateDefaultPreview(image);
return image;
}
}

View File

@@ -1,153 +0,0 @@
package eu.crushedpixel.replaymod.video.frame;
import eu.crushedpixel.replaymod.settings.RenderOptions;
import eu.crushedpixel.replaymod.video.entity.CubicEntityRenderer;
import net.minecraft.crash.CrashReport;
import net.minecraft.util.ReportedException;
import net.minecraft.util.Timer;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import static java.lang.Math.PI;
public class EquirectangularFrameRenderer extends FrameRenderer {
private static final byte IMAGE_BACK = 0;
private static final byte IMAGE_FRONT = 1;
private static final byte IMAGE_LEFT = 2;
private static final byte IMAGE_RIGHT = 3;
private static final byte IMAGE_TOP = 4;
private static final byte IMAGE_BOTTOM = 5;
private final int frameSize;
protected final CubicEntityRenderer entityRenderer;
private byte[][] image;
private double[][] imageX;
private double[][] imageY;
public EquirectangularFrameRenderer(RenderOptions options, boolean stable) {
super(options);
this.frameSize = options.getWidth() / 4;
this.entityRenderer = new CubicEntityRenderer(options, frameSize, stable);
setCustomEntityRenderer(entityRenderer);
}
private void computeCubicToEquirectangularTables() {
int rWidth = getVideoWidth();
int rHeight = getVideoHeight();
image = new byte[rWidth][rHeight];
imageX = new double[rWidth][rHeight];
imageY = new double[rWidth][rHeight];
for (int i = 0; i < rWidth; i++) {
double yaw = PI * 2 * i / rWidth;
int piQuarter = 8 * i / rWidth - 4;
byte target;
if (piQuarter < -3) {
target = IMAGE_BACK;
} else if (piQuarter < -1) {
target = IMAGE_LEFT;
} else if (piQuarter < 1) {
target = IMAGE_FRONT;
} else if (piQuarter < 3) {
target = IMAGE_RIGHT;
} else {
target = IMAGE_BACK;
}
double fYaw = (yaw + PI/4) % (PI / 2) - PI/4;
double d = 1 / Math.cos(fYaw);
double gcXN = (Math.tan(fYaw) + 1) / 2;
for (int j = 0; j < rHeight; j++) {
double cXN = gcXN;
byte pt = target;
double pitch = PI * j / rHeight - PI / 2;
double cYN = (Math.tan(pitch) * d + 1) / 2;
if (cYN >= 1) {
double pd = Math.tan(PI/2 - pitch);
cXN = (-Math.sin(yaw) * pd + 1) / 2;
cYN = (Math.cos(yaw) * pd + 1) / 2;
pt = IMAGE_BOTTOM;
}
if (cYN < 0) {
double pd = Math.tan(PI/2 - pitch);
cXN = (Math.sin(yaw) * pd + 1) / 2;
cYN = (Math.cos(yaw) * pd + 1) / 2;
pt = IMAGE_TOP;
}
image[i][j] = pt;
imageX[i][j] = Math.min(frameSize - 1, (cXN * frameSize));
imageY[i][j] = Math.min(frameSize - 1, (cYN * frameSize));
}
}
}
@Override
public BufferedImage captureFrame(Timer timer) {
BufferedImage top = captureFrame(timer, CubicEntityRenderer.Direction.TOP);
BufferedImage bottom = captureFrame(timer, CubicEntityRenderer.Direction.BOTTOM);
BufferedImage front = captureFrame(timer, CubicEntityRenderer.Direction.FRONT);
BufferedImage back = captureFrame(timer, CubicEntityRenderer.Direction.BACK);
BufferedImage left = captureFrame(timer, CubicEntityRenderer.Direction.LEFT);
BufferedImage right = captureFrame(timer, CubicEntityRenderer.Direction.RIGHT);
try {
BufferedImage result = cubicToEquirectangular(top, bottom, front, back, left, right);
updateDefaultPreview(result);
return result;
} catch (Throwable t) {
CrashReport crash = CrashReport.makeCrashReport(t, "Transforming cubic to equirectangular image.");
throw new ReportedException(crash);
}
}
protected BufferedImage cubicToEquirectangular(BufferedImage top, BufferedImage bottom, BufferedImage front, BufferedImage back, BufferedImage left, BufferedImage right) {
int[][] images = {
((DataBufferInt) back.getRaster().getDataBuffer()).getData(),
((DataBufferInt) front.getRaster().getDataBuffer()).getData(),
((DataBufferInt) left.getRaster().getDataBuffer()).getData(),
((DataBufferInt) right.getRaster().getDataBuffer()).getData(),
((DataBufferInt) top.getRaster().getDataBuffer()).getData(),
((DataBufferInt) bottom.getRaster().getDataBuffer()).getData()
};
int fWidth = frameSize;
int rWidth = getVideoWidth();
int rHeight = getVideoHeight();
if (image == null || image.length != rWidth || image[0].length != rHeight) {
try {
computeCubicToEquirectangularTables();
} catch (Throwable t) {
t.printStackTrace();
}
}
BufferedImage result = new BufferedImage(rWidth, rHeight, BufferedImage.TYPE_INT_RGB);
int[] resultPixels = ((DataBufferInt) result.getRaster().getDataBuffer()).getData();
byte[] image;
double[] imageX, imageY;
for (int i = 0; i < rWidth; i++) {
image = this.image[i];
imageX = this.imageX[i];
imageY = this.imageY[i];
for (int j = 0; j < rHeight; j++) {
resultPixels[i + j * rWidth] = images[image[j]][(int) imageX[j] + (int) imageY[j] * fWidth];
}
}
return result;
}
protected BufferedImage captureFrame(Timer timer, CubicEntityRenderer.Direction direction) {
try {
BufferedImage image = new BufferedImage(frameSize, frameSize, BufferedImage.TYPE_INT_RGB);
entityRenderer.setDirection(direction);
renderFrame(timer, image, 0, 0);
return image;
} catch (Throwable t) {
CrashReport crash = CrashReport.makeCrashReport(t, "Rendering frame " + direction);
throw new ReportedException(crash);
}
}
}

View File

@@ -1,149 +0,0 @@
package eu.crushedpixel.replaymod.video.frame;
import eu.crushedpixel.replaymod.settings.RenderOptions;
import eu.crushedpixel.replaymod.utils.BoundingUtils;
import eu.crushedpixel.replaymod.video.entity.CustomEntityRenderer;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.Gui;
import net.minecraft.client.renderer.texture.DynamicTexture;
import net.minecraft.client.renderer.texture.TextureUtil;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.Timer;
import org.lwjgl.BufferUtils;
import org.lwjgl.util.Dimension;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.nio.ByteBuffer;
import java.util.Arrays;
import static com.google.common.base.Preconditions.checkNotNull;
import static net.minecraft.client.renderer.GlStateManager.bindTexture;
import static net.minecraft.client.renderer.GlStateManager.color;
public abstract class FrameRenderer {
private static final ResourceLocation noPreviewTexture = new ResourceLocation("replaymod", "logo.jpg");
protected final Minecraft mc = Minecraft.getMinecraft();
protected final RenderOptions options;
private CustomEntityRenderer customEntityRenderer;
private DynamicTexture previewTexture;
private boolean previewActive = false;
protected final ByteBuffer buffer;
public FrameRenderer(RenderOptions options) {
this.options = options;
this.buffer = BufferUtils.createByteBuffer(options.getWidth() * options.getHeight() * 3);
}
public final int getVideoWidth() {
return options.getWidth();
}
public final int getVideoHeight() {
return options.getHeight();
}
public void setPreviewActive(boolean previewActive) {
this.previewActive = previewActive;
if (previewActive) {
if (previewTexture != null) {
Arrays.fill(previewTexture.getTextureData(), 0xff000000);
previewTexture.updateDynamicTexture();
}
}
}
public boolean isPreviewActive() {
return previewActive;
}
public void setCustomEntityRenderer(CustomEntityRenderer customEntityRenderer) {
this.customEntityRenderer = checkNotNull(customEntityRenderer);
}
public abstract BufferedImage captureFrame(Timer timer);
protected void updateDefaultPreview(BufferedImage image) {
if (isPreviewActive()) {
int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
updateDefaultPreview(pixels);
}
}
protected void updateDefaultPreview(int[] pixels) {
if (isPreviewActive() && previewTexture != null) {
System.arraycopy(pixels, 0, previewTexture.getTextureData(), 0, pixels.length);
previewTexture.updateDynamicTexture();
}
}
protected void renderFrame(Timer timer, BufferedImage into, int x, int y) {
customEntityRenderer.renderFrame(timer.renderPartialTicks, into, x, y);
}
public void setup() {
if (customEntityRenderer == null) {
customEntityRenderer = new CustomEntityRenderer(options) {};
}
previewTexture = new DynamicTexture(getVideoWidth(), getVideoHeight()) {
@Override
public void updateDynamicTexture() {
bindTexture(getGlTextureId());
TextureUtil.uploadTextureSub(0, getTextureData(), getVideoWidth(), getVideoHeight(), 0, 0, true, false, false);
}
};
}
public void tearDown() {
if (previewTexture != null) {
previewTexture.deleteGlTexture();
}
if (customEntityRenderer != null) {
customEntityRenderer.cleanup();
}
}
public void drawPreviewTexture(int x, int y, int width, int height) {
int videoWidth = getVideoWidth();
int videoHeight = getVideoHeight();
color(1, 1, 1, 1);
if (previewTexture != null) {
bindTexture(previewTexture.getGlTextureId());
Gui.drawScaledCustomSizeModalRect(x, y, 0, 0, videoWidth, videoHeight, width, height, videoWidth, videoHeight);
}
}
/**
* Draw a preview of the current scene within the specified box.
* @param x Left border of the box
* @param y Upper border of the box
* @param width Width of the box
* @param height Height of the box
*/
public void renderPreview(int x, int y, int width, int height) {
Dimension dimension = BoundingUtils.fitIntoBounds(new Dimension(getVideoWidth(), getVideoHeight()), new Dimension(width, height));
x += (width - dimension.getWidth()) / 2;
y += (height - dimension.getHeight()) / 2;
drawPreviewTexture(x, y, dimension.getWidth(), dimension.getHeight());
}
public static void renderNoPreview(int x, int y, int width, int height) {
int actualWidth = width;
int actualHeight = height;
if (width / height > 1280 / 720) {
actualWidth = height * 1280 / 720;
} else {
actualHeight = width * 720 / 1280;
}
x += (width - actualWidth) / 2;
y += (height - actualHeight) / 2;
Minecraft.getMinecraft().getTextureManager().bindTexture(noPreviewTexture);
color(1, 1, 1, 1);
Gui.drawScaledCustomSizeModalRect(x, y, 0, 0, 1280, 720, actualWidth, actualHeight, 1280, 720);
}
}

View File

@@ -0,0 +1,20 @@
package eu.crushedpixel.replaymod.video.frame;
import eu.crushedpixel.replaymod.video.rendering.Frame;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.lwjgl.util.ReadableDimension;
import java.nio.ByteBuffer;
@RequiredArgsConstructor
public class OpenGlFrame implements Frame {
@Getter
private final int frameId;
@Getter
private final ReadableDimension size;
@Getter
private final ByteBuffer byteBuffer;
}

View File

@@ -1,40 +0,0 @@
package eu.crushedpixel.replaymod.video.frame;
import eu.crushedpixel.replaymod.settings.RenderOptions;
import eu.crushedpixel.replaymod.video.entity.StereoscopicEntityRenderer;
import net.minecraft.crash.CrashReport;
import net.minecraft.util.ReportedException;
import net.minecraft.util.Timer;
import java.awt.image.BufferedImage;
public class StereoscopicFrameRenderer extends FrameRenderer {
private final StereoscopicEntityRenderer entityRenderer;
public StereoscopicFrameRenderer(RenderOptions options) {
super(options);
entityRenderer = new StereoscopicEntityRenderer(options);
setCustomEntityRenderer(entityRenderer);
}
@Override
public BufferedImage captureFrame(Timer timer) {
BufferedImage image = new BufferedImage(getVideoWidth(), getVideoHeight(), BufferedImage.TYPE_INT_RGB);
try {
entityRenderer.setEye(true);
renderFrame(timer, image, 0, 0);
} catch (Throwable t) {
CrashReport crash = CrashReport.makeCrashReport(t, "Rendering left eye.");
throw new ReportedException(crash);
}
try {
entityRenderer.setEye(false);
renderFrame(timer, image, getVideoWidth() / 2, 0);
} catch (Throwable t) {
CrashReport crash = CrashReport.makeCrashReport(t, "Rendering right eye.");
throw new ReportedException(crash);
}
updateDefaultPreview(image);
return image;
}
}

View File

@@ -0,0 +1,22 @@
package eu.crushedpixel.replaymod.video.frame;
import eu.crushedpixel.replaymod.video.rendering.Frame;
import lombok.Getter;
import org.apache.commons.lang3.Validate;
public class StereoscopicOpenGlFrame implements Frame {
@Getter
private final OpenGlFrame left, right;
public StereoscopicOpenGlFrame(OpenGlFrame left, OpenGlFrame right) {
Validate.isTrue(left.getFrameId() == right.getFrameId(), "Frame ids do not match.");
Validate.isTrue(left.getByteBuffer().remaining() == right.getByteBuffer().remaining(), "Buffer size does not match.");
this.left = left;
this.right = right;
}
@Override
public int getFrameId() {
return left.getFrameId();
}
}

View File

@@ -0,0 +1,12 @@
package eu.crushedpixel.replaymod.video.processor;
import eu.crushedpixel.replaymod.video.rendering.Frame;
import eu.crushedpixel.replaymod.video.rendering.FrameProcessor;
import java.io.IOException;
public abstract class AbstractFrameProcessor<R extends Frame, P extends Frame> implements FrameProcessor<R, P> {
@Override
public void close() throws IOException {
}
}

View File

@@ -0,0 +1,33 @@
package eu.crushedpixel.replaymod.video.processor;
import eu.crushedpixel.replaymod.utils.ByteBufferPool;
import eu.crushedpixel.replaymod.video.frame.ARGBFrame;
import eu.crushedpixel.replaymod.video.frame.CubicOpenGlFrame;
import org.lwjgl.util.Dimension;
import java.nio.ByteBuffer;
import static eu.crushedpixel.replaymod.utils.OpenGLUtils.openGlBytesToARBG;
public class CubicToARGBProcessor extends AbstractFrameProcessor<CubicOpenGlFrame, ARGBFrame> {
@Override
public ARGBFrame process(CubicOpenGlFrame rawFrame) {
int size = rawFrame.getLeft().getSize().getWidth();
int width = size * 4;
int height = size * 3;
ByteBuffer result = ByteBufferPool.allocate(width * height * 4);
openGlBytesToARBG(rawFrame.getLeft().getByteBuffer(), size, 0, size, result, width);
openGlBytesToARBG(rawFrame.getFront().getByteBuffer(), size, size, size, result, width);
openGlBytesToARBG(rawFrame.getRight().getByteBuffer(), size, size * 2, size, result, width);
openGlBytesToARBG(rawFrame.getBack().getByteBuffer(), size, size * 3, size, result, width);
openGlBytesToARBG(rawFrame.getTop().getByteBuffer(), size, size, 0, result, width);
openGlBytesToARBG(rawFrame.getBottom().getByteBuffer(), size, size, size * 2, result, width);
ByteBufferPool.release(rawFrame.getLeft().getByteBuffer());
ByteBufferPool.release(rawFrame.getRight().getByteBuffer());
ByteBufferPool.release(rawFrame.getFront().getByteBuffer());
ByteBufferPool.release(rawFrame.getBack().getByteBuffer());
ByteBufferPool.release(rawFrame.getTop().getByteBuffer());
ByteBufferPool.release(rawFrame.getBottom().getByteBuffer());
return new ARGBFrame(rawFrame.getFrameId(), new Dimension(width, height), result);
}
}

View File

@@ -0,0 +1,119 @@
package eu.crushedpixel.replaymod.video.processor;
import eu.crushedpixel.replaymod.utils.ByteBufferPool;
import eu.crushedpixel.replaymod.video.frame.ARGBFrame;
import eu.crushedpixel.replaymod.video.frame.CubicOpenGlFrame;
import org.apache.commons.lang3.Validate;
import org.lwjgl.util.Dimension;
import java.nio.ByteBuffer;
import static java.lang.Math.PI;
public class EquirectangularToARGBProcessor extends AbstractFrameProcessor<CubicOpenGlFrame, ARGBFrame> {
private static final byte IMAGE_BACK = 0;
private static final byte IMAGE_FRONT = 1;
private static final byte IMAGE_LEFT = 2;
private static final byte IMAGE_RIGHT = 3;
private static final byte IMAGE_TOP = 4;
private static final byte IMAGE_BOTTOM = 5;
private final int frameSize;
private final int width;
private final int height;
private final byte[][] image;
private final int[][] imageX;
private final int[][] imageY;
public EquirectangularToARGBProcessor(int frameSize) {
this.frameSize = frameSize;
width = frameSize * 4;
height = frameSize * 2;
image = new byte[height][width];
imageX = new int[height][width];
imageY = new int[height][width];
for (int i = 0; i < width; i++) {
double yaw = PI * 2 * i / width;
int piQuarter = 8 * i / width - 4;
byte target;
if (piQuarter < -3) {
target = IMAGE_BACK;
} else if (piQuarter < -1) {
target = IMAGE_LEFT;
} else if (piQuarter < 1) {
target = IMAGE_FRONT;
} else if (piQuarter < 3) {
target = IMAGE_RIGHT;
} else {
target = IMAGE_BACK;
}
double fYaw = (yaw + PI/4) % (PI / 2) - PI/4;
double d = 1 / Math.cos(fYaw);
double gcXN = (Math.tan(fYaw) + 1) / 2;
for (int j = 0; j < height; j++) {
double cXN = gcXN;
byte pt = target;
double pitch = PI * j / height - PI / 2;
double cYN = (Math.tan(pitch) * d + 1) / 2;
if (cYN >= 1) {
double pd = Math.tan(PI/2 - pitch);
cXN = (-Math.sin(yaw) * pd + 1) / 2;
cYN = (Math.cos(yaw) * pd + 1) / 2;
pt = IMAGE_BOTTOM;
}
if (cYN < 0) {
double pd = Math.tan(PI/2 - pitch);
cXN = (Math.sin(yaw) * pd + 1) / 2;
cYN = (Math.cos(yaw) * pd + 1) / 2;
pt = IMAGE_TOP;
}
int imgX = (int) Math.min(frameSize - 1, (cXN * frameSize));
int imgY = (int) Math.min(frameSize - 1, (cYN * frameSize));
image[j][i] = pt;
imageX[j][i] = imgX;
imageY[j][i] = frameSize - imgY - 1; // The OpenGl buffer contains data flipped vertically
}
}
}
@Override
public ARGBFrame process(CubicOpenGlFrame rawFrame) {
Validate.isTrue(rawFrame.getLeft().getSize().getWidth() == frameSize, "Frame size must be %d but was %d",
frameSize, rawFrame.getLeft().getSize().getWidth());
ByteBuffer result = ByteBufferPool.allocate(width * height * 4);
ByteBuffer[] images = {
rawFrame.getBack().getByteBuffer(), rawFrame.getFront().getByteBuffer(),
rawFrame.getLeft().getByteBuffer(), rawFrame.getRight().getByteBuffer(),
rawFrame.getTop().getByteBuffer(), rawFrame.getBottom().getByteBuffer()
};
byte[] pixel = new byte[4];
pixel[0] = (byte) 0xff;
byte[] image;
int[] imageX, imageY;
for (int y = 0; y < height; y++) {
image = this.image[y];
imageX = this.imageX[y];
imageY = this.imageY[y];
for (int x = 0; x < width; x++) {
ByteBuffer source = images[image[x]];
source.position((imageX[x] + imageY[x] * frameSize) * 3);
source.get(pixel, 1, 3);
result.put(pixel);
}
}
result.rewind();
ByteBufferPool.release(rawFrame.getLeft().getByteBuffer());
ByteBufferPool.release(rawFrame.getRight().getByteBuffer());
ByteBufferPool.release(rawFrame.getFront().getByteBuffer());
ByteBufferPool.release(rawFrame.getBack().getByteBuffer());
ByteBufferPool.release(rawFrame.getTop().getByteBuffer());
ByteBufferPool.release(rawFrame.getBottom().getByteBuffer());
return new ARGBFrame(rawFrame.getFrameId(), new Dimension(width, height), result);
}
}

View File

@@ -0,0 +1,22 @@
package eu.crushedpixel.replaymod.video.processor;
import eu.crushedpixel.replaymod.utils.ByteBufferPool;
import eu.crushedpixel.replaymod.video.frame.ARGBFrame;
import eu.crushedpixel.replaymod.video.frame.OpenGlFrame;
import org.lwjgl.util.ReadableDimension;
import java.nio.ByteBuffer;
import static eu.crushedpixel.replaymod.utils.OpenGLUtils.openGlBytesToARBG;
public class OpenGlToARGBProcessor extends AbstractFrameProcessor<OpenGlFrame, ARGBFrame> {
@Override
public ARGBFrame process(OpenGlFrame rawFrame) {
ReadableDimension size = rawFrame.getSize();
ByteBuffer buffer = rawFrame.getByteBuffer();
ByteBuffer argb = ByteBufferPool.allocate(size.getWidth() * size.getHeight() * 4);
openGlBytesToARBG(buffer, size.getWidth(), 0, 0, argb, size.getWidth());
ByteBufferPool.release(buffer);
return new ARGBFrame(rawFrame.getFrameId(), size, argb);
}
}

View File

@@ -0,0 +1,27 @@
package eu.crushedpixel.replaymod.video.processor;
import eu.crushedpixel.replaymod.utils.ByteBufferPool;
import eu.crushedpixel.replaymod.video.frame.ARGBFrame;
import eu.crushedpixel.replaymod.video.frame.StereoscopicOpenGlFrame;
import org.lwjgl.util.Dimension;
import org.lwjgl.util.ReadableDimension;
import java.nio.ByteBuffer;
import static eu.crushedpixel.replaymod.utils.OpenGLUtils.openGlBytesToARBG;
public class StereoscopicToARGBProcessor extends AbstractFrameProcessor<StereoscopicOpenGlFrame, ARGBFrame> {
@Override
public ARGBFrame process(StereoscopicOpenGlFrame rawFrame) {
ReadableDimension size = rawFrame.getLeft().getSize();
int width = size.getWidth();
ByteBuffer leftBuffer = rawFrame.getLeft().getByteBuffer();
ByteBuffer rightBuffer = rawFrame.getRight().getByteBuffer();
ByteBuffer result = ByteBufferPool.allocate(width * 2 * size.getHeight() * 4);
openGlBytesToARBG(leftBuffer, width, 0, 0, result, width * 2);
openGlBytesToARBG(rightBuffer, width, size.getWidth(), 0, result, width * 2);
ByteBufferPool.release(leftBuffer);
ByteBufferPool.release(rightBuffer);
return new ARGBFrame(rawFrame.getFrameId(), new Dimension(width * 2, size.getHeight()), result);
}
}

View File

@@ -0,0 +1,5 @@
package eu.crushedpixel.replaymod.video.rendering;
public interface Frame {
int getFrameId();
}

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}
}
}
}

View File

@@ -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
);
}
}

View File

@@ -10,6 +10,7 @@ public net.minecraft.client.Minecraft field_71445_n # isGamePaused
public net.minecraft.client.Minecraft field_152351_aB # scheduledTasks
public net.minecraft.client.Minecraft field_110449_ao # defaultResourcePacks
public net.minecraft.client.Minecraft field_110448_aq # mcResourcePackRepository
public net.minecraft.client.Minecraft field_71434_R # hasCrashed
public net.minecraft.client.Minecraft func_71386_F()L # getSystemTime
public net.minecraft.client.Minecraft func_71383_b(I)V # updateDebugProfilerName