Separate rendering into its own module

This commit is contained in:
johni0702
2016-06-14 17:01:32 +02:00
parent 65cfdd13ec
commit 25582b6a51
68 changed files with 1822 additions and 2635 deletions

View File

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

View File

@@ -0,0 +1,11 @@
package com.replaymod.render.rendering;
import java.io.Closeable;
public interface FrameCapturer<R extends Frame> extends Closeable {
boolean isDone();
R process();
}

View File

@@ -0,0 +1,9 @@
package com.replaymod.render.rendering;
import java.io.Closeable;
public interface FrameConsumer<P extends Frame> extends Closeable {
void consume(P frame);
}

View File

@@ -0,0 +1,9 @@
package com.replaymod.render.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 com.replaymod.render.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,76 @@
package com.replaymod.render.rendering;
import com.replaymod.render.RenderSettings;
import com.replaymod.render.capturer.*;
import com.replaymod.render.frame.CubicOpenGlFrame;
import com.replaymod.render.frame.OpenGlFrame;
import com.replaymod.render.frame.RGBFrame;
import com.replaymod.render.frame.StereoscopicOpenGlFrame;
import com.replaymod.render.hooks.EntityRendererHandler;
import com.replaymod.render.processor.CubicToRGBProcessor;
import com.replaymod.render.processor.EquirectangularToRGBProcessor;
import com.replaymod.render.processor.OpenGlToRGBProcessor;
import com.replaymod.render.processor.StereoscopicToRGBProcessor;
import com.replaymod.render.utils.PixelBufferObject;
import lombok.experimental.UtilityClass;
@UtilityClass
public class Pipelines {
public static Pipeline newPipeline(RenderSettings.RenderMethod method, RenderInfo renderInfo, FrameConsumer<RGBFrame> consumer) {
switch (method) {
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 method: " + method);
}
public static Pipeline<OpenGlFrame, RGBFrame> newDefaultPipeline(RenderInfo renderInfo, FrameConsumer<RGBFrame> consumer) {
RenderSettings settings = renderInfo.getRenderSettings();
FrameCapturer<OpenGlFrame> capturer;
if (PixelBufferObject.SUPPORTED) {
capturer = new SimplePboOpenGlFrameCapturer(new EntityRendererHandler(settings), renderInfo);
} else {
capturer = new SimpleOpenGlFrameCapturer(new EntityRendererHandler(settings), renderInfo);
}
return new Pipeline<>(capturer, new OpenGlToRGBProcessor(), consumer);
}
public static Pipeline<StereoscopicOpenGlFrame, RGBFrame> newStereoscopicPipeline(RenderInfo renderInfo, FrameConsumer<RGBFrame> consumer) {
RenderSettings settings = renderInfo.getRenderSettings();
FrameCapturer<StereoscopicOpenGlFrame> capturer;
if (PixelBufferObject.SUPPORTED) {
capturer = new StereoscopicPboOpenGlFrameCapturer(new EntityRendererHandler(settings), renderInfo);
} else {
capturer = new StereoscopicOpenGlFrameCapturer(new EntityRendererHandler(settings), renderInfo);
}
return new Pipeline<>(capturer, new StereoscopicToRGBProcessor(), consumer);
}
public static Pipeline<CubicOpenGlFrame, RGBFrame> newCubicPipeline(RenderInfo renderInfo, FrameConsumer<RGBFrame> consumer) {
RenderSettings settings = renderInfo.getRenderSettings();
FrameCapturer<CubicOpenGlFrame> capturer;
if (PixelBufferObject.SUPPORTED) {
capturer = new CubicPboOpenGlFrameCapturer(new EntityRendererHandler(settings), renderInfo, settings.getVideoWidth() / 4);
} else {
capturer = new CubicOpenGlFrameCapturer(new EntityRendererHandler(settings), renderInfo, settings.getVideoWidth() / 4);
}
return new Pipeline<>(capturer, new CubicToRGBProcessor(), consumer);
}
public static Pipeline<CubicOpenGlFrame, RGBFrame> newEquirectangularPipeline(RenderInfo renderInfo, FrameConsumer<RGBFrame> consumer) {
RenderSettings settings = renderInfo.getRenderSettings();
FrameCapturer<CubicOpenGlFrame> capturer;
if (PixelBufferObject.SUPPORTED) {
capturer = new CubicPboOpenGlFrameCapturer(new EntityRendererHandler(settings), renderInfo, settings.getVideoWidth() / 4);
} else {
capturer = new CubicOpenGlFrameCapturer(new EntityRendererHandler(settings), renderInfo, settings.getVideoWidth() / 4);
}
return new Pipeline<>(capturer, new EquirectangularToRGBProcessor(settings.getVideoWidth() / 4), consumer);
}
}

View File

@@ -0,0 +1,308 @@
package com.replaymod.render.rendering;
import com.google.common.base.Optional;
import com.replaymod.core.ReplayMod;
import com.replaymod.pathing.path.Keyframe;
import com.replaymod.pathing.path.Path;
import com.replaymod.pathing.path.Timeline;
import com.replaymod.pathing.properties.TimestampProperty;
import com.replaymod.render.RenderSettings;
import com.replaymod.render.VideoWriter;
import com.replaymod.render.capturer.RenderInfo;
import com.replaymod.render.frame.RGBFrame;
import com.replaymod.render.gui.GuiVideoRenderer;
import com.replaymod.render.hooks.ChunkLoadingRenderGlobal;
import com.replaymod.render.hooks.RenderReplayTimer;
import com.replaymod.render.metadata.MetadataInjector;
import com.replaymod.replay.ReplayHandler;
import net.minecraft.client.Minecraft;
import net.minecraft.client.audio.SoundCategory;
import net.minecraft.client.gui.ScaledResolution;
import net.minecraft.client.renderer.OpenGlHelper;
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.io.IOException;
import java.util.Collection;
import java.util.EnumMap;
import java.util.Map;
import java.util.concurrent.FutureTask;
import static com.google.common.collect.Iterables.getLast;
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 implements RenderInfo {
private final Minecraft mc = Minecraft.getMinecraft();
private final RenderSettings settings;
private final ReplayHandler replayHandler;
private final Timeline timeline;
private final Pipeline renderingPipeline;
private final VideoWriter videoWriter;
private int fps;
private boolean mouseWasGrabbed;
private boolean debugInfoWasShown;
private Map originalSoundLevels;
private ChunkLoadingRenderGlobal chunkLoadingRenderGlobal;
private int framesDone;
private int totalFrames;
private final GuiVideoRenderer gui;
private boolean paused;
private boolean cancelled;
public VideoRenderer(RenderSettings settings, ReplayHandler replayHandler, Timeline timeline) throws IOException {
this.settings = settings;
this.replayHandler = replayHandler;
this.timeline = timeline;
this.gui = new GuiVideoRenderer(this);
this.renderingPipeline = Pipelines.newPipeline(settings.getRenderMethod(), this,
videoWriter = new VideoWriter(settings) {
@Override
public void consume(RGBFrame frame) {
gui.updatePreview(frame);
super.consume(frame);
}
});
}
/**
* Render this video.
* @return {@code true} if rendering was successful, {@code false} if the user aborted rendering (or the window was closed)
*/
public boolean renderVideo() {
setup();
// Because this might take some time to prepare we'll render the GUI at least once to not confuse the user
drawGui();
Timer timer = mc.timer;
// Play up to one second before starting to render
// This is necessary in order to ensure that all entities have at least two position packets
// and their first position in the recording is correct.
// Note that it is impossible to also get the interpolation between their latest position
// and the one in the recording correct as there's no reliable way to tell when the server ticks
// or when we should be done with the interpolation of the entity
Optional<Integer> optionalVideoStartTime = timeline.getValue(TimestampProperty.PROPERTY, 0);
if (optionalVideoStartTime.isPresent()) {
int videoStart = optionalVideoStartTime.get();
if (videoStart > 1000) {
int replayTime = videoStart - 1000;
timer.elapsedPartialTicks = timer.renderPartialTicks = 0;
timer.timerSpeed = 1;
while (replayTime < videoStart) {
timer.elapsedTicks = 1;
replayTime += 50;
replayHandler.getReplaySender().sendPacketsTill(replayTime);
tick();
}
}
}
mc.renderGlobal.renderEntitiesStartupCounter = 0;
renderingPipeline.run();
if (settings.isInject360Metadata()) {
MetadataInjector.inject360Metadata(settings.getOutputFile());
}
finish();
return !cancelled;
}
@Override
public float updateForNextFrame() {
if (!settings.isHighPerformance() || framesDone % fps == 0) {
drawGui();
}
timeline.applyToGame(getVideoTime(), replayHandler);
int elapsedTicks = mc.timer.elapsedTicks;
while (elapsedTicks-- > 0) {
tick();
}
framesDone++;
return mc.timer.renderPartialTicks;
}
@Override
public RenderSettings getRenderSettings() {
return settings;
}
private void setup() {
replayHandler.getReplaySender().setSyncModeAndWait();
if (!OpenGlHelper.isFramebufferEnabled()) {
Display.setResizable(false);
}
if (mc.gameSettings.showDebugInfo) {
debugInfoWasShown = true;
mc.gameSettings.showDebugInfo = false;
}
if (Mouse.isGrabbed()) {
mouseWasGrabbed = true;
}
Mouse.setGrabbed(false);
mc.timer = new RenderReplayTimer(mc.timer);
mc.timer.timerSpeed = 1;
// Mute all sounds except GUI sounds (buttons, etc.)
Map<SoundCategory, Float> mutedSounds = new EnumMap<>(SoundCategory.class);
for (SoundCategory category : SoundCategory.values()) {
mutedSounds.put(category, 0f);
}
originalSoundLevels = mc.gameSettings.mapSoundLevels;
mutedSounds.put(SoundCategory.MASTER, (Float) originalSoundLevels.get(SoundCategory.MASTER));
mc.gameSettings.mapSoundLevels = mutedSounds;
fps = settings.getFramesPerSecond();
long duration = 0;
for (Path path : timeline.getPaths()) {
if (!path.isActive()) continue;
// Prepare path interpolations
path.updateAll();
// Find end time
Collection<Keyframe> keyframes = path.getKeyframes();
if (keyframes.size() > 0) {
duration = Math.max(duration, getLast(keyframes).getTime());
}
}
totalFrames = (int) (duration*fps/1000);
ScaledResolution scaled = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
gui.toMinecraft().setWorldAndResolution(mc, scaled.getScaledWidth(), scaled.getScaledHeight());
chunkLoadingRenderGlobal = new ChunkLoadingRenderGlobal(mc.renderGlobal);
}
private void finish() {
replayHandler.getReplaySender().setAsyncMode(true);
if (!OpenGlHelper.isFramebufferEnabled()) {
Display.setResizable(true);
}
mc.gameSettings.showDebugInfo = debugInfoWasShown;
if (mouseWasGrabbed) {
mc.mouseHelper.grabMouseCursor();
}
mc.timer = ((RenderReplayTimer) mc.timer).getWrapped();
mc.gameSettings.mapSoundLevels = originalSoundLevels;
mc.displayGuiScreen(null);
if (chunkLoadingRenderGlobal != null) {
chunkLoadingRenderGlobal.uninstall();
}
ReplayMod.soundHandler.playRenderSuccessSound();
mc.displayGuiScreen(null);
}
private void tick() {
synchronized (mc.scheduledTasks) {
while (!mc.scheduledTasks.isEmpty()) {
((FutureTask) mc.scheduledTasks.poll()).run();
}
}
mc.currentScreen = gui.toMinecraft();
try {
mc.runTick();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void drawGui() {
do {
pushMatrix();
clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
enableTexture2D();
mc.getFramebuffer().bindFramebuffer(true);
mc.entityRenderer.setupOverlayRendering();
try {
gui.toMinecraft().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.toMinecraft().drawScreen(mouseX, mouseY, 0);
mc.getFramebuffer().unbindFramebuffer();
popMatrix();
pushMatrix();
mc.getFramebuffer().framebufferRender(mc.displayWidth, mc.displayHeight);
popMatrix();
// if not in high performance mode, update the gui size if screen size changed
// otherwise just swap the progress gui to screen
if (settings.isHighPerformance()) {
Display.update();
} else {
mc.updateDisplay();
}
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(settings.getVideoWidth(), settings.getVideoHeight());
}
public int getTotalFrames() {
return totalFrames;
}
public int getVideoTime() { return framesDone * 1000 / fps; }
public void setPaused(boolean paused) {
this.paused = paused;
}
public boolean isPaused() {
return paused;
}
public void cancel() {
videoWriter.abort();
this.cancelled = true;
renderingPipeline.cancel();
}
}