Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright (c) NeoForged and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/

package net.neoforged.fml.earlydisplay;

import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import net.neoforged.fml.earlydisplay.render.EarlyFramebuffer;
import net.neoforged.fml.earlydisplay.render.ElementShader;
import net.neoforged.fml.earlydisplay.render.MaterializedTheme;
import net.neoforged.fml.earlydisplay.render.RenderContext;
import net.neoforged.fml.earlydisplay.render.SimpleBufferBuilder;
import net.neoforged.fml.earlydisplay.render.backend.ELSDrawCollector;
import net.neoforged.fml.earlydisplay.render.backend.ELSRenderBackend;
import net.neoforged.fml.earlydisplay.render.backend.ELSRenderPipeline;
import net.neoforged.fml.earlydisplay.render.backend.VertexFormat;
import net.neoforged.fml.earlydisplay.theme.Theme;
import net.neoforged.fml.earlydisplay.theme.ThemeColor;
import org.jetbrains.annotations.Nullable;

public abstract class AbstractEarlyScreen {
private final String name;
protected final ELSRenderBackend backend;
protected final int screenWidth;
protected final int screenHeight;
protected final MaterializedTheme theme;
private final Map<String, ELSRenderPipeline> pipelines;
protected final EarlyFramebuffer framebuffer;
protected final SimpleBufferBuilder bufferBuilder;
protected int animationFrame = 0;
protected int offsetX = 0;
protected int offsetY = 0;
protected float scale = 1F;

protected AbstractEarlyScreen(String name, Supplier<ELSRenderBackend> backend, Theme theme, @Nullable Path externalThemeDirectory, int screenWidth, int screenHeight) {
this.name = name;
this.backend = backend.get();
this.screenWidth = screenWidth;
this.screenHeight = screenHeight;
this.theme = MaterializedTheme.materialize(this.backend, theme, externalThemeDirectory);
this.pipelines = buildPipelines(this.theme.shaders());
this.backend.preloadPipelines(this.pipelines.values());
this.framebuffer = new EarlyFramebuffer(this.backend, screenWidth, screenHeight);
this.bufferBuilder = new SimpleBufferBuilder("shared_error", 8192);
}

private static Map<String, ELSRenderPipeline> buildPipelines(Map<String, ElementShader> shaders) {
Map<String, ELSRenderPipeline> pipelines = new HashMap<>();
for (Map.Entry<String, ElementShader> entry : shaders.entrySet()) {
pipelines.put(entry.getKey(), new ELSRenderPipeline(
entry.getValue(),
VertexFormat.POS_TEX_COLOR,
VertexFormat.Mode.QUADS,
ElementShader.UNIFORM_SAMPLER0,
List.of(ElementShader.UNIFORM_SCREEN_SIZE)));
}
return pipelines;
}

protected final void renderToFramebuffer(ThemeColor clearColor) {
if (!this.backend.startFrame(this.framebuffer::resize)) {
return;
}

ELSDrawCollector collector = new ELSDrawCollector(this.backend);

// Fit the layout rectangle into the screen while maintaining aspect ratio
var desiredAspectRatio = this.screenWidth / (float) this.screenHeight;
var actualAspectRatio = this.framebuffer.width() / (float) this.framebuffer.height();
if (actualAspectRatio > desiredAspectRatio) {
// This means we are wider than the desired aspect ratio, and have to center horizontally
var actualWidth = desiredAspectRatio * this.framebuffer.height();
this.offsetX = (int) (this.framebuffer.width() - actualWidth) / 2;
this.offsetY = 0;
collector.setViewport(this.offsetX, 0, (int) actualWidth, this.framebuffer.height());
this.scale = (float) this.framebuffer.height() / this.screenHeight;
} else {
// This means we are taller than the desired aspect ratio, and have to center vertically
var actualHeight = this.framebuffer.width() / desiredAspectRatio;
this.offsetX = 0;
this.offsetY = (int) (this.framebuffer.height() - actualHeight) / 2;
collector.setViewport(0, offsetY, this.framebuffer.width(), (int) actualHeight);
this.scale = (float) this.framebuffer.width() / this.screenWidth;
}

RenderContext context = new RenderContext(this.backend, collector, this.bufferBuilder, this.theme, this.pipelines, this.screenWidth, this.screenHeight, this.offsetX, this.offsetY, this.scale, this.framebuffer.height(), this.animationFrame);
renderToFramebuffer(context);
collector.execute(this.name, this.framebuffer.texture(), clearColor, this.bufferBuilder.getGpuBuffer(), this.screenWidth, this.screenHeight);

this.backend.presentTexture(this.framebuffer.texture(), clearColor, this.framebuffer.width(), this.framebuffer.height());

this.bufferBuilder.endFrame();
}

protected abstract void renderToFramebuffer(RenderContext context);

public void close(boolean destroyBackend) {
this.theme.close();
this.framebuffer.close();
this.bufferBuilder.close();
if (destroyBackend) {
this.backend.close();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,8 @@

package net.neoforged.fml.earlydisplay;

import static org.lwjgl.glfw.GLFW.GLFW_CLIENT_API;
import static org.lwjgl.glfw.GLFW.GLFW_CONTEXT_CREATION_API;
import static org.lwjgl.glfw.GLFW.GLFW_CONTEXT_VERSION_MAJOR;
import static org.lwjgl.glfw.GLFW.GLFW_CONTEXT_VERSION_MINOR;
import static org.lwjgl.glfw.GLFW.GLFW_FALSE;
import static org.lwjgl.glfw.GLFW.GLFW_NATIVE_CONTEXT_API;
import static org.lwjgl.glfw.GLFW.GLFW_NO_ERROR;
import static org.lwjgl.glfw.GLFW.GLFW_OPENGL_API;
import static org.lwjgl.glfw.GLFW.GLFW_OPENGL_CORE_PROFILE;
import static org.lwjgl.glfw.GLFW.GLFW_OPENGL_DEBUG_CONTEXT;
import static org.lwjgl.glfw.GLFW.GLFW_OPENGL_FORWARD_COMPAT;
import static org.lwjgl.glfw.GLFW.GLFW_OPENGL_PROFILE;
import static org.lwjgl.glfw.GLFW.GLFW_PLATFORM;
import static org.lwjgl.glfw.GLFW.GLFW_PLATFORM_WAYLAND;
import static org.lwjgl.glfw.GLFW.GLFW_PLATFORM_X11;
Expand All @@ -28,25 +18,23 @@
import static org.lwjgl.glfw.GLFW.GLFW_X11_INSTANCE_NAME;
import static org.lwjgl.glfw.GLFW.glfwCreateWindow;
import static org.lwjgl.glfw.GLFW.glfwDefaultWindowHints;
import static org.lwjgl.glfw.GLFW.glfwDestroyWindow;
import static org.lwjgl.glfw.GLFW.glfwGetError;
import static org.lwjgl.glfw.GLFW.glfwGetMonitorPos;
import static org.lwjgl.glfw.GLFW.glfwGetPrimaryMonitor;
import static org.lwjgl.glfw.GLFW.glfwGetVideoMode;
import static org.lwjgl.glfw.GLFW.glfwGetWindowSize;
import static org.lwjgl.glfw.GLFW.glfwInit;
import static org.lwjgl.glfw.GLFW.glfwInitHint;
import static org.lwjgl.glfw.GLFW.glfwMakeContextCurrent;
import static org.lwjgl.glfw.GLFW.glfwMaximizeWindow;
import static org.lwjgl.glfw.GLFW.glfwPlatformSupported;
import static org.lwjgl.glfw.GLFW.glfwPollEvents;
import static org.lwjgl.glfw.GLFW.glfwSetWindowIcon;
import static org.lwjgl.glfw.GLFW.glfwSetWindowPos;
import static org.lwjgl.glfw.GLFW.glfwSetWindowSizeCallback;
import static org.lwjgl.glfw.GLFW.glfwShowWindow;
import static org.lwjgl.glfw.GLFW.glfwSwapInterval;
import static org.lwjgl.glfw.GLFW.glfwWindowHint;
import static org.lwjgl.glfw.GLFW.glfwWindowHintString;
import static org.lwjgl.opengl.GL32C.GL_TRUE;

import java.awt.Desktop;
import java.io.IOException;
Expand All @@ -61,19 +49,22 @@
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Supplier;
import joptsimple.OptionParser;
import net.neoforged.fml.ModLoadingIssue;
import net.neoforged.fml.earlydisplay.error.ErrorDisplay;
import net.neoforged.fml.earlydisplay.render.LoadingScreenRenderer;
import net.neoforged.fml.earlydisplay.render.backend.ELSRenderBackend;
import net.neoforged.fml.earlydisplay.render.backend.opengl.GlRenderer;
import net.neoforged.fml.earlydisplay.theme.Theme;
import net.neoforged.fml.earlydisplay.theme.ThemeIds;
import net.neoforged.fml.earlydisplay.theme.ThemeLoader;
Expand All @@ -84,10 +75,10 @@
import net.neoforged.fml.loading.progress.StartupNotificationManager;
import net.neoforged.neoforgespi.earlywindow.ImmediateWindowProvider;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.VisibleForTesting;
import org.lwjgl.PointerBuffer;
import org.lwjgl.glfw.GLFWImage;
import org.lwjgl.glfw.GLFWVidMode;
import org.lwjgl.opengl.GL;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.system.MemoryUtil;
import org.lwjgl.util.tinyfd.TinyFileDialogs;
Expand Down Expand Up @@ -116,7 +107,7 @@ public class DisplayWindow implements ImmediateWindowProvider {
private boolean borderless;
private Theme theme;

private ScheduledFuture<LoadingScreenRenderer> rendererFuture;
private Future<LoadingScreenRenderer> rendererFuture;

// The GL ID of the window. Used for all operations
private long window;
Expand Down Expand Up @@ -207,17 +198,22 @@ public void initialize(ProgramArgs arguments) {

initWindow();

this.rendererFuture = renderScheduler.schedule(() -> new LoadingScreenRenderer(
renderScheduler,
window,
theme,
getThemePath(),
() -> minecraftVersion,
() -> neoForgeVersion), 1, TimeUnit.MILLISECONDS);
this.rendererFuture = this.renderScheduler.schedule(() -> this.setupRenderer(() -> GlRenderer.setupBackend(this.window), true), 1, TimeUnit.MILLISECONDS);

updateProgress("Initializing Game Graphics");
}

private LoadingScreenRenderer setupRenderer(Supplier<ELSRenderBackend> backend, boolean setupAutoRender) {
return new LoadingScreenRenderer(
this.renderScheduler,
backend,
this.theme,
getThemePath(),
() -> this.minecraftVersion,
() -> this.neoForgeVersion,
setupAutoRender);
}

@Override
public void setMinecraftVersion(String version) {
minecraftVersion = version;
Expand Down Expand Up @@ -263,13 +259,6 @@ private static Path getThemePath() {
return FMLPaths.CONFIGDIR.get().resolve("fml");
}

// Called from NeoForge
public void renderToFramebuffer() {
if (rendererFuture.isDone()) {
rendererFuture.resultNow().renderToFramebuffer();
}
}

private static final String ERROR_URL = "https://links.neoforged.net/early-display-errors";

private final ReentrantLock crashLock = new ReentrantLock();
Expand Down Expand Up @@ -346,12 +335,7 @@ public void initWindow() {
// Set window hints for the new window we're gonna create.
// Start of flags copied from Vanilla Minecraft
glfwDefaultWindowHints();
glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API);
glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_NATIVE_CONTEXT_API);
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
GlRenderer.configureWindowHints();
glfwWindowHint(GLFW_SOFT_FULLSCREEN, borderless ? GLFW_TRUE : GLFW_FALSE);
// End of flags copied from Vanilla Minecraft
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
Expand All @@ -362,10 +346,6 @@ public void initWindow() {
String vanillaWindowTitle = "Minecraft*";
glfwWindowHintString(GLFW_X11_CLASS_NAME, vanillaWindowTitle);
glfwWindowHintString(GLFW_X11_INSTANCE_NAME, vanillaWindowTitle);
if (FMLConfig.getBoolConfigValue(FMLConfig.ConfigValue.DEBUG_OPENGL)) {
LOGGER.info("Requesting the creation of an OpenGL debug context");
glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GL_TRUE);
}

long primaryMonitor = glfwGetPrimaryMonitor();
if (primaryMonitor == 0) {
Expand Down Expand Up @@ -460,13 +440,38 @@ private static Optional<String> getLastGlfwError() {
return Optional.empty();
}

/**
* Hand-off the window to the vanilla game.
* Called on the main thread instead of the game's initialization.
*
* @return the Window we own.
*/
public long takeOverGlfwWindow() {
@VisibleForTesting
public long getWindowHandle() {
return this.window;
}

@Override
public void handOverToMinecraft(Supplier<Object> backend) {
handOverToMinecraft(backend, true);
}

@VisibleForTesting
public void handOverToMinecraft(Supplier<Object> backend, boolean destroyWindow) {
this.shutdownAutomaticRenderer(true);
if (destroyWindow) {
glfwDestroyWindow(this.window);
}

// Perform renderer re-init on the calling thread because the incoming B3D backend cannot move the context to another thread
this.rendererFuture = CompletableFuture.completedFuture(this.setupRenderer(() -> (ELSRenderBackend) backend.get(), false));
try {
this.repaintTick = this.rendererFuture.get(30, TimeUnit.SECONDS)::renderToScreen;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (TimeoutException e) {
dumpBackgroundThreadStack();
crashElegantly("Cannot hand over rendering to Minecraft! The background loading screen renderer seems stuck.");
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}

private ELSRenderBackend shutdownAutomaticRenderer(boolean destroyBackend) {
// While this should have happened already, wait for it now to continue
LoadingScreenRenderer renderer;
try {
Expand All @@ -476,14 +481,15 @@ public long takeOverGlfwWindow() {
} catch (TimeoutException e) {
dumpBackgroundThreadStack();
crashElegantly("We seem to be having trouble initializing the window, waited for 30 seconds");
return -1L; // crashElegantly will never return
throw new AssertionError(); // crashElegantly will never return
}

updateProgress("Initializing Game Graphics");

// Stop the automatic off-thread rendering to move the GL context back to the main thread (this thread)
try {
renderer.stopAutomaticRendering();
renderer.close(destroyBackend);
} catch (TimeoutException e) {
dumpBackgroundThreadStack();
crashElegantly("Cannot hand over rendering to Minecraft! The background loading screen renderer seems stuck.");
Expand All @@ -493,25 +499,7 @@ public long takeOverGlfwWindow() {

completeProgress();

glfwMakeContextCurrent(window);
// Set the title to what the game wants
glfwSwapInterval(0);
// Clean up our hooks
glfwSetWindowSizeCallback(window, null).close();
this.repaintTick = renderer::renderToScreen; // the repaint will continue to be called until the overlay takes over
return window;
}

/**
* <strong>Called from Neo</strong>
*
* @return The OpenGL texture id of the texture the early loading screen is being rendered into.
*/
public int getFramebufferTextureId() {
if (!rendererFuture.isDone()) {
throw new IllegalStateException("Initialization of the renderer has not completed yet.");
}
return rendererFuture.resultNow().getFramebufferTextureId();
return renderer.getBackend();
}

@Override
Expand Down Expand Up @@ -542,7 +530,7 @@ public void close() {
// Close the Render Scheduler thread
renderScheduler.shutdown();
try {
rendererFuture.get().close();
rendererFuture.get().close(true);
} catch (ExecutionException e) {
LOGGER.error("Cannot close renderer since it failed to initialize", e);
} catch (InterruptedException e) {
Expand All @@ -558,10 +546,10 @@ public void crash(String message) {

@Override
public void displayFatalErrorAndExit(List<ModLoadingIssue> issues, @Nullable Path modsFolder, @Nullable Path logFile, @Nullable Path crashReportFile) {
long windowId = this.takeOverGlfwWindow();
GL.createCapabilities();
ELSRenderBackend backend = this.shutdownAutomaticRenderer(false);
backend.acquireContextOwnership(true);
this.close();
ErrorDisplay.fatal(windowId, assetsDir, assetIndex, issues, modsFolder, logFile, crashReportFile);
ErrorDisplay.fatal(backend, assetsDir, assetIndex, issues, modsFolder, logFile, crashReportFile);
}

private static void dumpBackgroundThreadStack() {
Expand Down
Loading
Loading