diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/AbstractEarlyScreen.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/AbstractEarlyScreen.java new file mode 100644 index 000000000..429d23879 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/AbstractEarlyScreen.java @@ -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 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 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 buildPipelines(Map shaders) { + Map pipelines = new HashMap<>(); + for (Map.Entry 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(); + } + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java index a8c8d1f76..52cc3ed1e 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java @@ -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; @@ -28,6 +18,7 @@ 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; @@ -35,7 +26,6 @@ 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; @@ -43,10 +33,8 @@ 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; @@ -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; @@ -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; @@ -116,7 +107,7 @@ public class DisplayWindow implements ImmediateWindowProvider { private boolean borderless; private Theme theme; - private ScheduledFuture rendererFuture; + private Future rendererFuture; // The GL ID of the window. Used for all operations private long window; @@ -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 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; @@ -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(); @@ -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); @@ -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) { @@ -460,13 +440,38 @@ private static Optional 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 backend) { + handOverToMinecraft(backend, true); + } + + @VisibleForTesting + public void handOverToMinecraft(Supplier 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 { @@ -476,7 +481,7 @@ 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"); @@ -484,6 +489,7 @@ public long takeOverGlfwWindow() { // 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."); @@ -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; - } - - /** - * Called from Neo - * - * @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 @@ -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) { @@ -558,10 +546,10 @@ public void crash(String message) { @Override public void displayFatalErrorAndExit(List 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() { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/error/Button.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/error/Button.java index 9b7170083..b3ecc6959 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/error/Button.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/error/Button.java @@ -12,6 +12,7 @@ import net.neoforged.fml.earlydisplay.render.RenderContext; import net.neoforged.fml.earlydisplay.render.SimpleFont; import net.neoforged.fml.earlydisplay.render.Texture; +import net.neoforged.fml.earlydisplay.render.backend.ELSRenderBackend; import net.neoforged.fml.earlydisplay.theme.ImageLoader; import net.neoforged.fml.earlydisplay.theme.NativeBuffer; import net.neoforged.fml.earlydisplay.theme.TextureScaling; @@ -77,7 +78,7 @@ void press() { } } - static Texture loadTexture(boolean active, boolean highlighted) { + static Texture loadTexture(ELSRenderBackend backend, boolean active, boolean highlighted) { String suffix = active ? (highlighted ? "_highlighted" : "") : "_disabled"; String fileName = "assets/minecraft/textures/gui/sprites/widget/button" + suffix + ".png"; String debugName = "error_button" + suffix; @@ -90,17 +91,17 @@ static Texture loadTexture(boolean active, boolean highlighted) { case ImageLoader.Result.Success success -> { // Sizes are deliberately double the real values to ensure the buttons render identical to vanilla in an environment with double the resolution TextureScaling scaling = new TextureScaling.NineSlice(400, 40, 6, 6, 6, 6, false, false, false); - yield Texture.create(success.image(), debugName, scaling, null); + yield Texture.create(backend, success.image(), debugName, scaling, null); } - case ImageLoader.Result.Error ignored -> createFallbackTexture(active, highlighted, suffix); + case ImageLoader.Result.Error ignored -> createFallbackTexture(backend, active, highlighted, suffix); }; } catch (IOException e) { - texture = createFallbackTexture(active, highlighted, suffix); + texture = createFallbackTexture(backend, active, highlighted, suffix); } return texture; } - private static Texture createFallbackTexture(boolean active, boolean highlighted, String suffix) { + private static Texture createFallbackTexture(ELSRenderBackend backend, boolean active, boolean highlighted, String suffix) { String name = "error_button_fallback" + suffix; int width = 60; int height = 20; @@ -121,6 +122,6 @@ private static Texture createFallbackTexture(boolean active, boolean highlighted UncompressedImage image = new UncompressedImage(name, null, buffer, width, height); // Sizes are deliberately double the real values to ensure the buttons render identical to vanilla in an environment with double the resolution TextureScaling scaling = new TextureScaling.NineSlice(width * 2, height * 2, 2, 2, 2, 2, true, true, false); - return Texture.create(image, name, scaling, null); + return Texture.create(backend, image, name, scaling, null); } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/error/ErrorDisplay.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/error/ErrorDisplay.java index 48b3f5c53..2d5a61fbd 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/error/ErrorDisplay.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/error/ErrorDisplay.java @@ -9,7 +9,7 @@ import java.util.List; import java.util.concurrent.TimeUnit; import net.neoforged.fml.ModLoadingIssue; -import net.neoforged.fml.earlydisplay.render.GlState; +import net.neoforged.fml.earlydisplay.render.backend.ELSRenderBackend; import org.jetbrains.annotations.Nullable; import org.lwjgl.glfw.GLFW; import org.lwjgl.system.Callback; @@ -20,7 +20,7 @@ public final class ErrorDisplay { private static final long MINFRAMETIME = TimeUnit.MILLISECONDS.toNanos(10); // This is the FPS cap on the window public static void fatal( - long windowHandle, + ELSRenderBackend backend, @Nullable String assetsDir, @Nullable String assetIndex, List errors, @@ -28,18 +28,18 @@ public static void fatal( @Nullable Path logFile, @Nullable Path crashReportFile) { // Pre-clear all callbacks that may be left-over from the previous owner of the window + long windowHandle = backend.getWindowHandle(); clearCallbacks(windowHandle); - ErrorDisplayWindow window = new ErrorDisplayWindow(windowHandle, assetsDir, assetIndex, errors, modsFolder, logFile, crashReportFile); + ErrorDisplayWindow window = new ErrorDisplayWindow(backend, assetsDir, assetIndex, errors, modsFolder, logFile, crashReportFile); - discard(GLFW.glfwSetWindowCloseCallback(window.windowHandle, window::handleClose)); - discard(GLFW.glfwSetCursorPosCallback(window.windowHandle, window::handleCursorPos)); - discard(GLFW.glfwSetScrollCallback(window.windowHandle, window::handleMouseScroll)); - discard(GLFW.glfwSetMouseButtonCallback(window.windowHandle, window::handleMouseButton)); - discard(GLFW.glfwSetKeyCallback(window.windowHandle, window::handleKey)); + discard(GLFW.glfwSetWindowCloseCallback(windowHandle, window::handleClose)); + discard(GLFW.glfwSetCursorPosCallback(windowHandle, window::handleCursorPos)); + discard(GLFW.glfwSetScrollCallback(windowHandle, window::handleMouseScroll)); + discard(GLFW.glfwSetMouseButtonCallback(windowHandle, window::handleMouseButton)); + discard(GLFW.glfwSetKeyCallback(windowHandle, window::handleKey)); long nextFrameTime = 0; - GlState.readFromOpenGL(); while (!window.isClosed()) { long nanoTime = System.nanoTime(); var timeToNextFrame = nextFrameTime - nanoTime; @@ -51,7 +51,7 @@ public static void fatal( GLFW.glfwWaitEventsTimeout(timeToNextFrame / (double) TimeUnit.SECONDS.toNanos(1)); } } - window.teardown(); + window.close(true); if (THROW_ON_EXIT) { throw new FatalLoadingError(); diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/error/ErrorDisplayWindow.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/error/ErrorDisplayWindow.java index 76c67511a..42dbf3734 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/error/ErrorDisplayWindow.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/error/ErrorDisplayWindow.java @@ -11,21 +11,18 @@ import java.util.function.BiFunction; import java.util.function.Function; import net.neoforged.fml.ModLoadingIssue; -import net.neoforged.fml.earlydisplay.render.EarlyFramebuffer; -import net.neoforged.fml.earlydisplay.render.ElementShader; -import net.neoforged.fml.earlydisplay.render.GlState; -import net.neoforged.fml.earlydisplay.render.MaterializedTheme; +import net.neoforged.fml.earlydisplay.AbstractEarlyScreen; import net.neoforged.fml.earlydisplay.render.RenderContext; -import net.neoforged.fml.earlydisplay.render.SimpleBufferBuilder; import net.neoforged.fml.earlydisplay.render.SimpleFont; import net.neoforged.fml.earlydisplay.render.Texture; +import net.neoforged.fml.earlydisplay.render.backend.ELSRenderBackend; import net.neoforged.fml.earlydisplay.theme.Theme; +import net.neoforged.fml.earlydisplay.theme.ThemeColor; import net.neoforged.fml.i18n.FMLTranslations; import org.jetbrains.annotations.Nullable; import org.lwjgl.glfw.GLFW; -import org.lwjgl.opengl.GL11C; -final class ErrorDisplayWindow { +final class ErrorDisplayWindow extends AbstractEarlyScreen { private static final int DISPLAY_WIDTH = 854; private static final int DISPLAY_HEIGHT = 480; private static final int BUTTON_WIDTH = 320; @@ -53,13 +50,10 @@ final class ErrorDisplayWindow { private static final int LIST_ENTRY_X = 30; private static final int LIST_CONTENT_WIDTH = DISPLAY_WIDTH - (LIST_ENTRY_X * 2); private static final int SCROLL_SPEED = 10; + private static final ThemeColor CLEAR_COLOR = ThemeColor.ofArgb(0xFF000000); - final long windowHandle; - private final MaterializedTheme theme; private final SimpleFont font; private final int errorLineHeight; - private final EarlyFramebuffer framebuffer; - private final SimpleBufferBuilder bufferBuilder; final Texture buttonTexture; final Texture buttonTextureHover; final Texture buttonTextureInactive; @@ -68,32 +62,26 @@ final class ErrorDisplayWindow { private final List entries; private final int totalEntryHeight; private boolean closed = false; - private int offsetX = 0; - private int offsetY = 0; - private float scale = 1F; private double mouseX = -1; private double mouseY = -1; private float scrollOffset = 0; private boolean draggingScrollbar = false; ErrorDisplayWindow( - long windowHandle, + ELSRenderBackend backend, @Nullable String assetsDir, @Nullable String assetIndex, List issues, @Nullable Path modsFolder, @Nullable Path logFile, @Nullable Path crashReportFile) { - this.windowHandle = windowHandle; - this.theme = MaterializedTheme.materialize(Theme.createDefaultTheme(), null); - SimpleFont mcFont = FontLoader.loadVanillaFont(assetsDir, assetIndex); + super("FML Error Screen", () -> backend, Theme.createDefaultTheme(), null, DISPLAY_WIDTH, DISPLAY_HEIGHT); + SimpleFont mcFont = FontLoader.loadVanillaFont(backend, assetsDir, assetIndex); this.font = mcFont != null ? mcFont : theme.getFont(Theme.FONT_DEFAULT); this.errorLineHeight = font.lineSpacing() - 5; - this.framebuffer = new EarlyFramebuffer(DISPLAY_WIDTH, DISPLAY_HEIGHT); - this.bufferBuilder = new SimpleBufferBuilder("shared_error", 8192); - this.buttonTexture = Button.loadTexture(true, false); - this.buttonTextureHover = Button.loadTexture(true, true); - this.buttonTextureInactive = Button.loadTexture(false, false); + this.buttonTexture = Button.loadTexture(backend, true, false); + this.buttonTextureHover = Button.loadTexture(backend, true, true); + this.buttonTextureInactive = Button.loadTexture(backend, false, false); boolean translate = mcFont != null; BiFunction translator = translate ? FMLTranslations::parseMessage : FMLTranslations::parseEnglishMessage; FileOpener opener = FileOpener.get(); @@ -148,55 +136,11 @@ private static void translateEntries(List issues, List desiredAspectRatio) { - // This means we are wider than the desired aspect ratio, and have to center horizontally - float actualWidth = desiredAspectRatio * framebuffer.height(); - offsetX = (int) (framebuffer.width() - actualWidth) / 2; - offsetY = 0; - scale = (float) framebuffer.height() / DISPLAY_HEIGHT; - GlState.viewport(offsetX, 0, (int) actualWidth, framebuffer.height()); - } else { - // This means we are taller than the desired aspect ratio, and have to center vertically - float actualHeight = framebuffer.width() / desiredAspectRatio; - offsetX = 0; - offsetY = (int) (framebuffer.height() - actualHeight) / 2; - scale = (float) framebuffer.width() / DISPLAY_WIDTH; - GlState.viewport(0, offsetY, framebuffer.width(), (int) actualHeight); - } - - GlState.clearColor(0F, 0F, 0F, 1F); - GL11C.glClear(GL11C.GL_COLOR_BUFFER_BIT | GL11C.GL_DEPTH_BUFFER_BIT); - GlState.enableBlend(true); - GlState.blendFuncSeparate(GL11C.GL_SRC_ALPHA, GL11C.GL_ONE_MINUS_SRC_ALPHA, GL11C.GL_ZERO, GL11C.GL_ONE); - - RenderContext ctx = new RenderContext(bufferBuilder, theme, DISPLAY_WIDTH, DISPLAY_HEIGHT, offsetX, offsetY, scale, 0); - for (ElementShader shader : theme.shaders().values()) { - shader.activate(); - if (shader.hasUniform(ElementShader.UNIFORM_SCREEN_SIZE)) { - shader.setUniform2f(ElementShader.UNIFORM_SCREEN_SIZE, DISPLAY_WIDTH, DISPLAY_HEIGHT); - } - } - - renderToFramebuffer(ctx); - - framebuffer.deactivate(); - - GlState.viewport(0, 0, framebuffer.width(), framebuffer.height()); - framebuffer.blitToScreen(theme.theme().colorScheme().screenBackground(), framebuffer.width(), framebuffer.height()); - GLFW.glfwSwapBuffers(windowHandle); + this.renderToFramebuffer(CLEAR_COLOR); } - private void renderToFramebuffer(RenderContext ctx) { + @Override + protected void renderToFramebuffer(RenderContext ctx) { // Background ctx.fillRect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT, 0xFF402020, 0xFF501010); // Top edge @@ -215,8 +159,7 @@ private void renderToFramebuffer(RenderContext ctx) { ctx.renderTextWithShadow(x, y, font, line.parts); } - GlState.scissorTest(true); - ctx.scissorBox(0, LIST_CONTENT_Y_TOP, DISPLAY_WIDTH, LIST_CONTENT_HEIGHT); + ctx.enableScissor(0, LIST_CONTENT_Y_TOP, DISPLAY_WIDTH, LIST_CONTENT_HEIGHT); float y = LIST_Y_TOP - scrollOffset; for (MessageEntry entry : entries) { float entryHeight = errorLineHeight * entry.lineCount(); @@ -243,7 +186,7 @@ private void renderToFramebuffer(RenderContext ctx) { } y += ENTRY_PADDING; } - GlState.scissorTest(false); + ctx.collector().disableScissor(); if (totalEntryHeight > LIST_CONTENT_HEIGHT) { float scrollFactor = scrollOffset / (totalEntryHeight - LIST_CONTENT_HEIGHT - 1); @@ -361,14 +304,12 @@ boolean isClosed() { return closed; } - void teardown() { - theme.close(); - framebuffer.close(); - bufferBuilder.close(); + @Override + public void close(boolean destroyBackend) { buttonTexture.close(); buttonTextureHover.close(); buttonTextureInactive.close(); - SimpleBufferBuilder.destroy(); + super.close(destroyBackend); } private record HeaderLine(List parts, int width) { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/error/FontLoader.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/error/FontLoader.java index 249a4ab4d..43479415e 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/error/FontLoader.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/error/FontLoader.java @@ -22,13 +22,13 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import net.neoforged.fml.earlydisplay.render.SimpleFont; -import net.neoforged.fml.earlydisplay.render.Texture; +import net.neoforged.fml.earlydisplay.render.backend.ELSRenderBackend; +import net.neoforged.fml.earlydisplay.render.backend.ELSTexture; +import net.neoforged.fml.earlydisplay.render.backend.TextureFormat; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.Nullable; import org.lwjgl.BufferUtils; -import org.lwjgl.opengl.GL11C; -import org.lwjgl.opengl.GL30C; /** * Loader for the GNU Unifont font definitions included in vanilla MC @@ -43,7 +43,7 @@ final class FontLoader { private static final int GLYPH_HEIGHT = 16; @Nullable - static SimpleFont loadVanillaFont(@Nullable String assetsDir, @Nullable String assetIndex) { + static SimpleFont loadVanillaFont(ELSRenderBackend backend, @Nullable String assetsDir, @Nullable String assetIndex) { if (assetsDir == null || assetIndex == null) { return null; } @@ -93,7 +93,7 @@ static SimpleFont loadVanillaFont(@Nullable String assetsDir, @Nullable String a } } - return glyphs.isEmpty() ? null : buildFont(glyphs); + return glyphs.isEmpty() ? null : buildFont(backend, glyphs); } private static void parseHexFile(InputStream stream, Consumer glyphOutput) throws IOException { @@ -150,7 +150,7 @@ private static int parseHexDigit(char digit) { } @Nullable - private static SimpleFont buildFont(List glyphs) { + private static SimpleFont buildFont(ELSRenderBackend backend, List glyphs) { int totalPixels = glyphs.stream().mapToInt(g -> g.width * GLYPH_HEIGHT).sum(); boolean incWidth = true; int texWidth = 512; @@ -163,7 +163,7 @@ private static SimpleFont buildFont(List glyphs) { } incWidth = !incWidth; } - int maxTexSize = GL11C.glGetInteger(GL11C.GL_MAX_TEXTURE_SIZE); + int maxTexSize = backend.getMaxTextureSize(); if (texWidth > maxTexSize || texHeight > maxTexSize) { // Abort if the GPU does not support the texture size required to fit the font atlas return null; @@ -202,14 +202,14 @@ private static SimpleFont buildFont(List glyphs) { } } - int textureId = Texture.createEmpty("unifont texture", texWidth, texHeight, GL30C.GL_R8, GL11C.GL_RED, false); - Texture.writeToTexture(textureId, texWidth, texHeight, GL11C.GL_RED, 1, bitmap); + ELSTexture texture = backend.createTexture("unifont_texture", texWidth, texHeight, TextureFormat.RED, false); + backend.writeToTexture(texture, bitmap); // SimpleFont relies on at least a space character being present if (!glyphMap.containsKey((int) ' ')) { return null; } - return new SimpleFont(24, GLYPH_HEIGHT, textureId, glyphMap::get); + return new SimpleFont(24, GLYPH_HEIGHT, texture, glyphMap::get); } private record ProtoGlyph(int codepoint, int width, int[] lines) {} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/EarlyFramebuffer.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/EarlyFramebuffer.java index 3358bb675..757d7ecaf 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/EarlyFramebuffer.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/EarlyFramebuffer.java @@ -5,104 +5,38 @@ package net.neoforged.fml.earlydisplay.render; -import static org.lwjgl.opengl.GL32C.GL_COLOR_ATTACHMENT0; -import static org.lwjgl.opengl.GL32C.GL_COLOR_BUFFER_BIT; -import static org.lwjgl.opengl.GL32C.GL_FRAMEBUFFER; -import static org.lwjgl.opengl.GL32C.GL_NEAREST; -import static org.lwjgl.opengl.GL32C.GL_RGBA; -import static org.lwjgl.opengl.GL32C.GL_TEXTURE_2D; -import static org.lwjgl.opengl.GL32C.GL_TEXTURE_MAG_FILTER; -import static org.lwjgl.opengl.GL32C.GL_TEXTURE_MIN_FILTER; -import static org.lwjgl.opengl.GL32C.GL_UNSIGNED_BYTE; -import static org.lwjgl.opengl.GL32C.glBlitFramebuffer; -import static org.lwjgl.opengl.GL32C.glClear; -import static org.lwjgl.opengl.GL32C.glDeleteFramebuffers; -import static org.lwjgl.opengl.GL32C.glDeleteTextures; -import static org.lwjgl.opengl.GL32C.glFramebufferTexture2D; -import static org.lwjgl.opengl.GL32C.glGenFramebuffers; -import static org.lwjgl.opengl.GL32C.glGenTextures; -import static org.lwjgl.opengl.GL32C.glTexImage2D; -import static org.lwjgl.opengl.GL32C.glTexParameteri; +import net.neoforged.fml.earlydisplay.render.backend.ELSRenderBackend; +import net.neoforged.fml.earlydisplay.render.backend.ELSTexture; +import net.neoforged.fml.earlydisplay.render.backend.TextureFormat; -import java.nio.IntBuffer; -import net.neoforged.fml.earlydisplay.render.elements.RenderElement; -import net.neoforged.fml.earlydisplay.theme.ThemeColor; - -public class EarlyFramebuffer { - private final int framebuffer; - private final int texture; +public final class EarlyFramebuffer { + private final ELSRenderBackend backend; + private ELSTexture texture; private int width; private int height; - public EarlyFramebuffer(int width, int height) { + public EarlyFramebuffer(ELSRenderBackend backend, int width, int height) { + this.backend = backend; this.width = width; this.height = height; - this.framebuffer = glGenFramebuffers(); - this.texture = glGenTextures(); - GlState.bindFramebuffer(this.framebuffer); - GlDebug.labelFramebuffer(this.framebuffer, "EarlyDisplay framebuffer"); - - GlState.bindTexture2D(this.texture); - GlDebug.labelTexture(this.texture, "EarlyDisplay backbuffer"); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, (IntBuffer) null); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, this.texture, 0); - GlState.bindFramebuffer(0); - } - - public void activate() { - GlState.bindFramebuffer(this.framebuffer); - } - - public void deactivate() { - GlState.bindFramebuffer(0); - } - - public void blitToScreen(ThemeColor backgroundColor, int windowFBWidth, int windowFBHeight) { - var wscale = ((float) windowFBWidth / width); - var hscale = ((float) windowFBHeight / height); - var scale = Math.min(wscale, hscale) / 2f; - var wleft = (int) (windowFBWidth * 0.5f - scale * width); - var wtop = (int) (windowFBHeight * 0.5f - scale * height); - var wright = (int) (windowFBWidth * 0.5f + scale * width); - var wbottom = (int) (windowFBHeight * 0.5f + scale * height); - GlState.bindDrawFramebuffer(0); - GlState.bindReadFramebuffer(this.framebuffer); - GlState.clearColor(backgroundColor.r(), backgroundColor.g(), backgroundColor.b(), 1f); - glClear(GL_COLOR_BUFFER_BIT); - // src Y are flipped, since our FB is flipped - glBlitFramebuffer( - 0, - height, - width, - 0, - RenderElement.clamp(wleft, 0, windowFBWidth), - RenderElement.clamp(wtop, 0, windowFBHeight), - RenderElement.clamp(wright, 0, windowFBWidth), - RenderElement.clamp(wbottom, 0, windowFBHeight), - GL_COLOR_BUFFER_BIT, - GL_NEAREST); - GlState.bindFramebuffer(0); + this.texture = backend.createTexture("EarlyDisplay Framebuffer", width, height, TextureFormat.RGBA, false); } - int getTexture() { + public ELSTexture texture() { return this.texture; } public void resize(int width, int height) { if (this.width != width || this.height != height) { - GlState.bindFramebuffer(framebuffer); - GlState.bindTexture2D(texture); + this.texture.close(); + this.texture = this.backend.createTexture("EarlyDisplay Framebuffer", width, height, TextureFormat.RGBA, false); this.width = width; this.height = height; - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, (IntBuffer) null); } } public void close() { - glDeleteTextures(this.texture); - glDeleteFramebuffers(this.framebuffer); + this.texture.close(); } public int width() { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/ElementShader.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/ElementShader.java index 340435b18..8001c4daa 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/ElementShader.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/ElementShader.java @@ -5,165 +5,46 @@ package net.neoforged.fml.earlydisplay.render; -import static org.lwjgl.opengl.GL32C.GL_ACTIVE_UNIFORMS; -import static org.lwjgl.opengl.GL32C.GL_COMPILE_STATUS; -import static org.lwjgl.opengl.GL32C.GL_FALSE; -import static org.lwjgl.opengl.GL32C.GL_FRAGMENT_SHADER; -import static org.lwjgl.opengl.GL32C.GL_LINK_STATUS; -import static org.lwjgl.opengl.GL32C.GL_VERTEX_SHADER; -import static org.lwjgl.opengl.GL32C.glAttachShader; -import static org.lwjgl.opengl.GL32C.glBindAttribLocation; -import static org.lwjgl.opengl.GL32C.glCompileShader; -import static org.lwjgl.opengl.GL32C.glCreateProgram; -import static org.lwjgl.opengl.GL32C.glCreateShader; -import static org.lwjgl.opengl.GL32C.glDeleteProgram; -import static org.lwjgl.opengl.GL32C.glDeleteShader; -import static org.lwjgl.opengl.GL32C.glDetachShader; -import static org.lwjgl.opengl.GL32C.glGetActiveUniformName; -import static org.lwjgl.opengl.GL32C.glGetProgramInfoLog; -import static org.lwjgl.opengl.GL32C.glGetProgrami; -import static org.lwjgl.opengl.GL32C.glGetShaderInfoLog; -import static org.lwjgl.opengl.GL32C.glGetShaderi; -import static org.lwjgl.opengl.GL32C.glLinkProgram; -import static org.lwjgl.opengl.GL32C.glShaderSource; -import static org.lwjgl.opengl.GL32C.glUniform1i; -import static org.lwjgl.opengl.GL32C.glUniform2f; - import java.io.IOException; -import java.nio.ByteBuffer; import java.nio.file.Path; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; +import net.neoforged.fml.earlydisplay.theme.NativeBuffer; import net.neoforged.fml.earlydisplay.theme.ThemeResource; import org.jetbrains.annotations.Nullable; -import org.lwjgl.PointerBuffer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -public class ElementShader implements AutoCloseable { +public class ElementShader { public static final String UNIFORM_SCREEN_SIZE = "screenSize"; public static final String UNIFORM_SAMPLER0 = "tex"; - private static final Logger LOGGER = LoggerFactory.getLogger(ElementShader.class); - private final String name; - private int program; - private final Map uniformLocations; - private final Set warnedAboutUniforms = new HashSet<>(); + private final ThemeResource vertexShader; + private final ThemeResource fragmentShader; + private final @Nullable Path externalThemeDirectory; - private ElementShader(String name, int program, Map uniformLocations) { + public ElementShader(String name, ThemeResource vertexShader, ThemeResource fragmentShader, @Nullable Path externalThemeDirectory) { this.name = name; - this.program = program; - this.uniformLocations = uniformLocations; + this.vertexShader = vertexShader; + this.fragmentShader = fragmentShader; + this.externalThemeDirectory = externalThemeDirectory; } - public static ElementShader create(String name, ThemeResource vertexShader, ThemeResource fragmentShader, @Nullable Path externalThemeDirectory) { - try (var vertexShaderBuffer = vertexShader.toNativeBuffer(externalThemeDirectory); - var fragmentShaderBuffer = fragmentShader.toNativeBuffer(externalThemeDirectory)) { - return create(name, vertexShaderBuffer.buffer(), fragmentShaderBuffer.buffer()); - } catch (IOException e) { - throw new RuntimeException("Failed to read shaders for " + name); - } - } - - public static ElementShader create(String name, ByteBuffer vertexShaderSource, ByteBuffer fragmentShaderSource) { - int vertexShader = glCreateShader(GL_VERTEX_SHADER); - GlDebug.labelShader(vertexShader, "FML " + name + ".vert"); - int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); - GlDebug.labelShader(fragmentShader, "FML " + name + ".frag"); - - // Bind the source of our shaders to the ones created above - var sourcePointers = PointerBuffer.allocateDirect(1); - sourcePointers.put(0, fragmentShaderSource); - glShaderSource(fragmentShader, sourcePointers, new int[] { fragmentShaderSource.remaining() }); - sourcePointers.put(0, vertexShaderSource); - glShaderSource(vertexShader, sourcePointers, new int[] { vertexShaderSource.remaining() }); - - // Compile the vertex and fragment elementShader so that we can use them - glCompileShader(vertexShader); - if (glGetShaderi(vertexShader, GL_COMPILE_STATUS) == GL_FALSE) { - throw new IllegalStateException("VertexShader linkage failure. \n" + glGetShaderInfoLog(vertexShader)); - } - glCompileShader(fragmentShader); - if (glGetShaderi(fragmentShader, GL_COMPILE_STATUS) == GL_FALSE) { - throw new IllegalStateException("FragmentShader linkage failure. \n" + glGetShaderInfoLog(fragmentShader)); - } - - var program = glCreateProgram(); - GlDebug.labelProgram(program, "EarlyDisplay program"); - glBindAttribLocation(program, 0, "position"); - glBindAttribLocation(program, 1, "uv"); - glBindAttribLocation(program, 2, "color"); - glAttachShader(program, vertexShader); - glAttachShader(program, fragmentShader); - glLinkProgram(program); - if (glGetProgrami(program, GL_LINK_STATUS) == GL_FALSE) { - throw new RuntimeException("ShaderProgram linkage failure. \n" + glGetProgramInfoLog(program)); - } - - glDetachShader(program, vertexShader); - glDetachShader(program, fragmentShader); - glDeleteShader(vertexShader); - glDeleteShader(fragmentShader); - - var uniformCount = glGetProgrami(program, GL_ACTIVE_UNIFORMS); - Map uniformLocations = new HashMap<>(uniformCount); - for (var i = 0; i < uniformCount; i++) { - String uniformName = glGetActiveUniformName(program, i); - uniformLocations.put(uniformName, i); - } - - return new ElementShader(name, program, uniformLocations); + public String getName() { + return this.name; } - public void activate() { - GlState.useProgram(program); + public String getVertexShaderPath() { + return this.vertexShader.path(); } - public void clear() { - GlState.useProgram(0); + public String getFragmentShaderPath() { + return this.fragmentShader.path(); } - public boolean hasUniform(String name) { - return uniformLocations.containsKey(name); - } - - public void setUniform1i(String name, int value) { - var location = uniformLocations.get(name); - if (location != null) { - glUniform1i(location, value); - } else { - warnAboutMissingUniform(name); - } - } - - public void setUniform2f(String name, float x, float y) { - var location = uniformLocations.get(name); - if (location != null) { - glUniform2f(location, x, y); - } else { - warnAboutMissingUniform(name); - } - } - - private void warnAboutMissingUniform(String name) { - if (warnedAboutUniforms.add(name)) { - LOGGER.error("Missing uniform '{}' in shader '{}'", name, this); - } - } - - @Override - public void close() { - if (program > 0) { - glDeleteProgram(program); - program = 0; - } + public NativeBuffer loadVertexShader() throws IOException { + return this.vertexShader.toNativeBuffer(this.externalThemeDirectory); } - public int program() { - return program; + public NativeBuffer loadFragmentShader() throws IOException { + return this.fragmentShader.toNativeBuffer(this.externalThemeDirectory); } @Override diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java index d332feda2..685485a6a 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java @@ -5,33 +5,19 @@ package net.neoforged.fml.earlydisplay.render; -import static org.lwjgl.glfw.GLFW.glfwGetFramebufferSize; -import static org.lwjgl.glfw.GLFW.glfwMakeContextCurrent; -import static org.lwjgl.glfw.GLFW.glfwSwapBuffers; -import static org.lwjgl.glfw.GLFW.glfwSwapInterval; -import static org.lwjgl.opengl.GL.createCapabilities; -import static org.lwjgl.opengl.GL11C.GL_COLOR_BUFFER_BIT; -import static org.lwjgl.opengl.GL11C.GL_DEPTH_BUFFER_BIT; -import static org.lwjgl.opengl.GL11C.GL_ONE; -import static org.lwjgl.opengl.GL11C.GL_ONE_MINUS_SRC_ALPHA; -import static org.lwjgl.opengl.GL11C.GL_RENDERER; -import static org.lwjgl.opengl.GL11C.GL_SRC_ALPHA; -import static org.lwjgl.opengl.GL11C.GL_VENDOR; -import static org.lwjgl.opengl.GL11C.GL_VERSION; -import static org.lwjgl.opengl.GL11C.GL_ZERO; -import static org.lwjgl.opengl.GL11C.glClear; -import static org.lwjgl.opengl.GL11C.glGetString; - import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Supplier; +import net.neoforged.fml.earlydisplay.AbstractEarlyScreen; +import net.neoforged.fml.earlydisplay.render.backend.ELSRenderBackend; import net.neoforged.fml.earlydisplay.render.elements.ImageElement; import net.neoforged.fml.earlydisplay.render.elements.LabelElement; import net.neoforged.fml.earlydisplay.render.elements.MojangLogoElement; @@ -40,114 +26,113 @@ import net.neoforged.fml.earlydisplay.render.elements.RenderElement; import net.neoforged.fml.earlydisplay.render.elements.StartupLogElement; import net.neoforged.fml.earlydisplay.theme.Theme; +import net.neoforged.fml.earlydisplay.theme.ThemeLoadingScreen; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeDecorativeElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemeElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemeImageElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemeLabelElement; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.VisibleForTesting; -import org.lwjgl.glfw.GLFW; -import org.lwjgl.opengl.GL; -import org.lwjgl.opengl.GL32C; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class LoadingScreenRenderer implements AutoCloseable { +public final class LoadingScreenRenderer extends AbstractEarlyScreen { private static final Logger LOGGER = LoggerFactory.getLogger(LoadingScreenRenderer.class); - public static final int LAYOUT_WIDTH = 854; - public static final int LAYOUT_HEIGHT = 480; + private static final int LAYOUT_WIDTH = 854; + private static final int LAYOUT_HEIGHT = 480; + private static final long MINFRAMETIME = TimeUnit.MILLISECONDS.toNanos(10); // This is the FPS cap on the window - note animation is capped at 20FPS via the tickTimer @VisibleForTesting public static volatile boolean rendered = false; - private final long glfwWindow; - private final MaterializedTheme theme; - - private static final long MINFRAMETIME = TimeUnit.MILLISECONDS.toNanos(10); // This is the FPS cap on the window - note animation is capped at 20FPS via the tickTimer - - private int animationFrame; - private long nextFrameTime = 0; - - private final EarlyFramebuffer framebuffer; - private final Semaphore renderLock = new Semaphore(1); - - // Scheduled background rendering of the loading screen - private final ScheduledFuture automaticRendering; - - private final List elements; - - private final SimpleBufferBuilder buffer = new SimpleBufferBuilder("shared", 8192); - private final Supplier minecraftVersion; private final Supplier neoForgeVersion; + // Scheduled background rendering of the loading screen + private final Future automaticRendering; + private final List elements; + private long lastFrameTime = 0; + private boolean closed = false; - /** - * Render initialization methods called by the Render Thread. - * It compiles the fragment and vertex shaders for rendering text with STB, and sets up basic render framework. - *

- * Nothing fancy, we just want to draw and render text. - */ - public LoadingScreenRenderer(ScheduledExecutorService scheduler, - long glfwWindow, + public LoadingScreenRenderer( + ScheduledExecutorService scheduler, + Supplier backend, Theme theme, @Nullable Path externalThemeDirectory, Supplier minecraftVersion, - Supplier neoForgeVersion) { - this.glfwWindow = glfwWindow; + Supplier neoForgeVersion, + boolean setupAutoRender) { + super("FML Early Loading Screen", backend, theme, externalThemeDirectory, LAYOUT_WIDTH, LAYOUT_HEIGHT); this.minecraftVersion = minecraftVersion; this.neoForgeVersion = neoForgeVersion; - - // This thread owns the GL render context now. We should make a note of that. - glfwMakeContextCurrent(glfwWindow); - // Wait for one frame to be complete before swapping; enable vsync in other words. - glfwSwapInterval(1); - var capabilities = createCapabilities(); - GlState.readFromOpenGL(); - GlDebug.setCapabilities(capabilities); - LOGGER.info("GL info: {} GL version {}, {}", glGetString(GL_RENDERER), glGetString(GL_VERSION), glGetString(GL_VENDOR)); - - // Create GL resources - this.theme = MaterializedTheme.materialize(theme, externalThemeDirectory); this.elements = loadElements(); + this.backend.releaseContextOwnership(); - // we always render to an 854x480 texture and then fit that to the screen - framebuffer = new EarlyFramebuffer(LAYOUT_WIDTH, LAYOUT_HEIGHT); - - // Set the clear color based on the colour scheme - var background = theme.colorScheme().screenBackground(); - GlState.clearColor(background.r(), background.g(), background.b(), 1f); - GL32C.glClear(GL_COLOR_BUFFER_BIT); - - GlState.enableBlend(true); - GlState.blendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glfwMakeContextCurrent(0); - this.automaticRendering = scheduler.scheduleWithFixedDelay(this::renderToScreen, 50, 50, TimeUnit.MILLISECONDS); + if (setupAutoRender) { + this.automaticRendering = scheduler.scheduleWithFixedDelay(this::renderAutomatic, 50, 50, TimeUnit.MILLISECONDS); + } else { + this.automaticRendering = new CompletableFuture<>(); + this.automaticRendering.cancel(true); + } // schedule a 50 ms ticker to try and smooth out the rendering scheduler.scheduleWithFixedDelay(() -> animationFrame++, 1, 50, TimeUnit.MILLISECONDS); } + private void renderAutomatic() { + if (!this.renderLock.tryAcquire()) { + return; + } + try { + this.backend.acquireContextOwnership(false); + this.renderToScreen(); + } finally { + this.backend.releaseContextOwnership(); // we release the gl context IF we're running off the main thread + this.renderLock.release(); + rendered = true; + } + } + + public void renderToScreen() { + try { + long nanoTime = System.nanoTime(); + if (nanoTime - this.lastFrameTime > MINFRAMETIME) { + this.lastFrameTime = nanoTime; + this.renderToFramebuffer(this.theme.theme().colorScheme().screenBackground()); + } + } catch (Throwable t) { + LOGGER.error("Unexpected error while rendering the loading screen", t); + } + } + + @Override + protected void renderToFramebuffer(RenderContext context) { + for (RenderElement element : this.elements) { + element.render(context); + } + } + private List loadElements() { - var elements = new ArrayList(); + List elements = new ArrayList<>(); - var loadingScreen = theme.theme().loadingScreen(); + ThemeLoadingScreen loadingScreen = this.theme.theme().loadingScreen(); if (loadingScreen.background() != null && loadingScreen.background().visible()) { - elements.add(new ImageElement(loadingScreen.background(), theme)); + elements.add(new ImageElement(this.backend, loadingScreen.background(), this.theme)); } if (loadingScreen.performance().visible()) { - elements.add(new PerformanceElement(loadingScreen.performance(), theme)); + elements.add(new PerformanceElement(loadingScreen.performance(), this.theme)); } if (loadingScreen.startupLog().visible()) { - elements.add(new StartupLogElement(loadingScreen.startupLog(), theme)); + elements.add(new StartupLogElement(loadingScreen.startupLog(), this.theme)); } if (loadingScreen.progressBars().visible()) { - elements.add(new ProgressBarsElement(loadingScreen.progressBars(), theme)); + elements.add(new ProgressBarsElement(loadingScreen.progressBars(), this.theme)); } if (loadingScreen.mojangLogo().visible()) { - elements.add(new MojangLogoElement(loadingScreen.mojangLogo(), theme)); + elements.add(new MojangLogoElement(this.backend, loadingScreen.mojangLogo(), this.theme)); } // Add decorative elements - for (var entry : loadingScreen.decoration().entrySet()) { - var element = entry.getValue(); + for (Map.Entry entry : loadingScreen.decoration().entrySet()) { + ThemeDecorativeElement element = entry.getValue(); if (!element.visible()) { continue; // Likely reconfigured in an extended theme } @@ -158,15 +143,9 @@ private List loadElements() { } private RenderElement loadElement(String id, ThemeElement element) { - var renderElement = switch (element) { - case ThemeImageElement imageElement -> new ImageElement(imageElement, theme); - - case ThemeLabelElement labelElement -> new LabelElement( - labelElement, - theme, - () -> Map.of( - "version", getVersionString())); - + RenderElement renderElement = switch (element) { + case ThemeImageElement imageElement -> new ImageElement(this.backend, imageElement, this.theme); + case ThemeLabelElement labelElement -> new LabelElement(labelElement, this.theme, () -> Map.of("version", getVersionString())); default -> throw new IllegalStateException("Unexpected theme element " + element + " of type " + element.getClass()); }; renderElement.setId(id); @@ -174,12 +153,12 @@ private RenderElement loadElement(String id, ThemeElement element) { } private String getVersionString() { - var result = new StringBuilder(); - var minecraftVersion = this.minecraftVersion.get(); + StringBuilder result = new StringBuilder(); + String minecraftVersion = this.minecraftVersion.get(); if (minecraftVersion != null) { result.append(minecraftVersion); } - var neoForgeVersion = this.neoForgeVersion.get(); + String neoForgeVersion = this.neoForgeVersion.get(); if (neoForgeVersion != null) { if (!result.isEmpty()) { result.append("-"); @@ -201,142 +180,28 @@ public void stopAutomaticRendering() throws TimeoutException, InterruptedExcepti // it skips releasing the context. The main thread then crashes when it attempts to take ownership of the context. // Since renderToScreen bails immediately without running GL calls if it fails to acquire the lock, // we can safely assume the above won't happen once we have acquired it. - if (!renderLock.tryAcquire(5, TimeUnit.SECONDS)) { + if (!this.renderLock.tryAcquire(5, TimeUnit.SECONDS)) { throw new TimeoutException(); } this.automaticRendering.cancel(false); - renderLock.release(); - } - - /** - * The main render loop. - * renderThread executes this. - *

- * Performs initialization and then ticks the screen at 20 fps. - * When the thread is killed, context is destroyed. - */ - public void renderToScreen() { - if (!renderLock.tryAcquire()) { - return; - } - try { - long nt; - if ((nt = System.nanoTime()) < nextFrameTime) { - return; - } - nextFrameTime = nt + MINFRAMETIME; - glfwMakeContextCurrent(glfwWindow); - - GlState.readFromOpenGL(); - var backup = GlState.createSnapshot(); - - int[] w = new int[1]; - int[] h = new int[1]; - glfwGetFramebufferSize(glfwWindow, w, h); - framebuffer.resize(w[0], h[0]); - - renderToFramebuffer(); - - GlState.viewport(0, 0, w[0], h[0]); - framebuffer.blitToScreen(this.theme.theme().colorScheme().screenBackground(), w[0], h[0]); - // Swap buffers; we're done - glfwSwapBuffers(glfwWindow); - - GlState.applySnapshot(backup); - } catch (Throwable t) { - LOGGER.error("Unexpected error while rendering the loading screen", t); - } finally { - if (!this.automaticRendering.isCancelled()) - glfwMakeContextCurrent(0); // we release the gl context IF we're running off the main thread - renderLock.release(); - rendered = true; - } - } - - public void renderToFramebuffer() { - GlDebug.pushGroup("update EarlyDisplay framebuffer"); - GlState.readFromOpenGL(); - var backup = GlState.createSnapshot(); - - framebuffer.activate(); - - // Fit the layout rectangle into the screen while maintaining aspect ratio - var desiredAspectRatio = LAYOUT_WIDTH / (float) LAYOUT_HEIGHT; - var actualAspectRatio = framebuffer.width() / (float) framebuffer.height(); - int offsetX; - int offsetY; - float scale; - if (actualAspectRatio > desiredAspectRatio) { - // This means we are wider than the desired aspect ratio, and have to center horizontally - var actualWidth = desiredAspectRatio * framebuffer.height(); - offsetX = (int) (framebuffer.width() - actualWidth) / 2; - offsetY = 0; - GlState.viewport(offsetX, 0, (int) actualWidth, framebuffer.height()); - scale = (float) framebuffer.height() / LAYOUT_HEIGHT; - } else { - // This means we are taller than the desired aspect ratio, and have to center vertically - var actualHeight = framebuffer.width() / desiredAspectRatio; - offsetX = 0; - offsetY = (int) (framebuffer.height() - actualHeight) / 2; - GlState.viewport(0, offsetY, framebuffer.width(), (int) actualHeight); - scale = (float) framebuffer.width() / LAYOUT_WIDTH; - } - - // Clear the screen to our color - var background = theme.theme().colorScheme().screenBackground(); - GlState.clearColor(background.r(), background.g(), background.b(), 1.0f); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - GlState.enableBlend(true); - GlState.blendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ZERO, GL_ONE); - - for (var shader : theme.shaders().values()) { - shader.activate(); - if (shader.hasUniform(ElementShader.UNIFORM_SCREEN_SIZE)) { - shader.setUniform2f(ElementShader.UNIFORM_SCREEN_SIZE, LAYOUT_WIDTH, LAYOUT_HEIGHT); - } - } - - var context = new RenderContext(buffer, theme, LAYOUT_WIDTH, LAYOUT_HEIGHT, offsetX, offsetY, scale, animationFrame); - - for (var element : this.elements) { - element.render(context); - } - - framebuffer.deactivate(); - - GlState.applySnapshot(backup); - GlDebug.popGroup(); + this.renderLock.release(); } @Override - public void close() { - var previousContext = GLFW.glfwGetCurrentContext(); - var previousCaps = GL.getCapabilities(); - - boolean needsToRestoreContext = false; - if (previousContext != glfwWindow) { - GLFW.glfwMakeContextCurrent(glfwWindow); - GL.createCapabilities(); - needsToRestoreContext = true; - } + public void close(boolean destroyBackend) { + if (!this.closed) { + this.closed = true; - try { - theme.close(); - for (var element : elements) { - element.close(); - } - framebuffer.close(); - buffer.close(); - SimpleBufferBuilder.destroy(); - } finally { - if (needsToRestoreContext) { - GLFW.glfwMakeContextCurrent(previousContext); - GL.setCapabilities(previousCaps); - } + this.backend.guardResourceCleanup(() -> { + for (RenderElement element : this.elements) { + element.close(); + } + super.close(destroyBackend); + }); } } - public int getFramebufferTextureId() { - return framebuffer.getTexture(); + public ELSRenderBackend getBackend() { + return this.backend; } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedTheme.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedTheme.java index f459f6806..7b4e676a4 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedTheme.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedTheme.java @@ -9,6 +9,7 @@ import java.nio.file.Path; import java.util.HashMap; import java.util.Map; +import net.neoforged.fml.earlydisplay.render.backend.ELSRenderBackend; import net.neoforged.fml.earlydisplay.theme.Theme; import net.neoforged.fml.earlydisplay.theme.ThemeResource; import net.neoforged.fml.earlydisplay.theme.ThemeShader; @@ -24,19 +25,19 @@ public record MaterializedTheme( Map fonts, Map shaders, MaterializedThemeSprites sprites) implements AutoCloseable { - public static MaterializedTheme materialize(Theme theme, @Nullable Path externalThemeDirectory) { + public static MaterializedTheme materialize(ELSRenderBackend backend, Theme theme, @Nullable Path externalThemeDirectory) { return new MaterializedTheme( theme, externalThemeDirectory, - loadFonts(theme.fonts(), externalThemeDirectory), + loadFonts(backend, theme.fonts(), externalThemeDirectory), loadShaders(theme.shaders(), externalThemeDirectory), - loadSprites(theme.sprites(), externalThemeDirectory)); + loadSprites(backend, theme.sprites(), externalThemeDirectory)); } private static Map loadShaders(Map themeShaders, @Nullable Path externalThemeDirectory) { var shaders = new HashMap(themeShaders.size()); for (var entry : themeShaders.entrySet()) { - var shader = ElementShader.create( + var shader = new ElementShader( entry.getKey(), entry.getValue().vertexShader(), entry.getValue().fragmentShader(), @@ -46,11 +47,11 @@ private static Map loadShaders(Map t return shaders; } - private static Map loadFonts(Map themeFonts, @Nullable Path externalThemeDirectory) { + private static Map loadFonts(ELSRenderBackend backend, Map themeFonts, @Nullable Path externalThemeDirectory) { var fonts = new HashMap(themeFonts.size()); for (var entry : themeFonts.entrySet()) { try { - fonts.put(entry.getKey(), new SimpleFont(entry.getValue(), externalThemeDirectory)); + fonts.put(entry.getKey(), new SimpleFont(backend, entry.getValue(), externalThemeDirectory)); } catch (IOException e) { throw new RuntimeException("Failed to load font " + entry.getKey(), e); } @@ -58,11 +59,11 @@ private static Map loadFonts(Map them return fonts; } - private static MaterializedThemeSprites loadSprites(ThemeSprites sprites, @Nullable Path externalThemeDirectory) { + private static MaterializedThemeSprites loadSprites(ELSRenderBackend backend, ThemeSprites sprites, @Nullable Path externalThemeDirectory) { return new MaterializedThemeSprites( - Texture.create(sprites.progressBarBackground(), externalThemeDirectory), - Texture.create(sprites.progressBarForeground(), externalThemeDirectory), - Texture.create(sprites.progressBarIndeterminate(), externalThemeDirectory)); + Texture.create(backend, sprites.progressBarBackground(), externalThemeDirectory), + Texture.create(backend, sprites.progressBarForeground(), externalThemeDirectory), + Texture.create(backend, sprites.progressBarIndeterminate(), externalThemeDirectory)); } public SimpleFont getFont(String fontId) { @@ -83,6 +84,7 @@ public ElementShader getShader(String shaderId) { @Override public void close() { - shaders.values().forEach(ElementShader::close); + this.sprites.close(); + this.fonts.values().forEach(SimpleFont::close); } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedThemeSprites.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedThemeSprites.java index a84848be7..6873c05d2 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedThemeSprites.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedThemeSprites.java @@ -8,4 +8,11 @@ public record MaterializedThemeSprites( Texture progressBarBackground, Texture progressBarForeground, - Texture progressBarIndeterminate) {} + Texture progressBarIndeterminate) implements AutoCloseable { + @Override + public void close() { + this.progressBarBackground.close(); + this.progressBarForeground.close(); + this.progressBarIndeterminate.close(); + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/QuadHelper.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/QuadHelper.java index 3d451d901..dfdf7966d 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/QuadHelper.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/QuadHelper.java @@ -195,16 +195,16 @@ public static void loadQuad(SimpleBufferBuilder bb, Bounds bounds, float u0, flo public static void loadQuad(SimpleBufferBuilder bb, float x0, float x1, float y0, float y1, float u0, float u1, float v0, float v1, int colour) { bb.pos(x0, y0).tex(u0, v0).colour(colour).endVertex(); - bb.pos(x1, y0).tex(u1, v0).colour(colour).endVertex(); bb.pos(x0, y1).tex(u0, v1).colour(colour).endVertex(); bb.pos(x1, y1).tex(u1, v1).colour(colour).endVertex(); + bb.pos(x1, y0).tex(u1, v0).colour(colour).endVertex(); } public static void loadQuad(SimpleBufferBuilder bb, float x0, float x1, float y0, float y1, float u0, float u1, float v0, float v1) { bb.pos(x0, y0).tex(u0, v0).endVertex(); - bb.pos(x1, y0).tex(u1, v0).endVertex(); bb.pos(x0, y1).tex(u0, v1).endVertex(); bb.pos(x1, y1).tex(u1, v1).endVertex(); + bb.pos(x1, y0).tex(u1, v0).endVertex(); } public enum SpriteFillDirection { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/RenderContext.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/RenderContext.java index fd6d858aa..68fa98e57 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/RenderContext.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/RenderContext.java @@ -7,25 +7,27 @@ import com.google.common.collect.Lists; import java.util.List; +import java.util.Map; +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.theme.Theme; import net.neoforged.fml.earlydisplay.theme.ThemeColor; import net.neoforged.fml.earlydisplay.util.Bounds; public record RenderContext( + ELSRenderBackend backend, + ELSDrawCollector collector, SimpleBufferBuilder sharedBuffer, MaterializedTheme theme, + Map pipelines, float availableWidth, float availableHeight, int viewportOffsetX, int viewportOffsetY, float viewportScale, + int windowHeight, int animationFrame) { - public ElementShader bindShader(String shaderId) { - var shader = theme.getShader(shaderId); - shader.activate(); - return shader; - } - public void blitTexture(Texture texture, Bounds bounds) { blitTexture(texture, bounds, -1); } @@ -42,7 +44,8 @@ public void blitTexture(Texture texture, float x, float y, float width, float he blitTextureRegion(texture, x, y, width, height, color, 0, 1, 0, 1); } - public void blitTextureRegion(Texture texture, + public void blitTextureRegion( + Texture texture, float x, float y, float width, @@ -52,13 +55,8 @@ public void blitTextureRegion(Texture texture, float u1, float v0, float v1) { - GlState.bindTexture2D(texture.textureId()); - GlState.bindSampler(0); - - var shader = bindShader(Theme.SHADER_GUI); - shader.setUniform1i(ElementShader.UNIFORM_SAMPLER0, 0); - - sharedBuffer.begin(SimpleBufferBuilder.Format.POS_TEX_COLOR, SimpleBufferBuilder.Mode.QUADS); + ELSRenderPipeline pipeline = this.pipelines.get(Theme.SHADER_GUI); + sharedBuffer.begin(pipeline.vertexFormat(), pipeline.vertexMode()); QuadHelper.fillSprite( sharedBuffer, @@ -76,7 +74,8 @@ public void blitTextureRegion(Texture texture, v0, v1); - sharedBuffer.draw(); + SimpleBufferBuilder.Result result = this.sharedBuffer.finishAndUpload(this.backend); + this.collector.submitDraw(pipeline, texture.texture(), result); } public void renderTextWithShadow(float x, float y, SimpleFont font, List texts) { @@ -86,12 +85,11 @@ public void renderTextWithShadow(float x, float y, SimpleFont font, List texts) { - GlState.bindTexture2D(font.textureId()); - GlState.bindSampler(0); - bindShader(Theme.SHADER_FONT); - sharedBuffer.begin(SimpleBufferBuilder.Format.POS_TEX_COLOR, SimpleBufferBuilder.Mode.QUADS); + ELSRenderPipeline pipeline = this.pipelines.get(Theme.SHADER_FONT); + sharedBuffer.begin(pipeline.vertexFormat(), pipeline.vertexMode()); font.generateVerticesForTexts(x, y, sharedBuffer, texts); - sharedBuffer.draw(); + SimpleBufferBuilder.Result result = this.sharedBuffer.finishAndUpload(this.backend); + this.collector.submitDraw(pipeline, font.texture(), result); } public void renderIndeterminateProgressBar(Bounds backgroundBounds) { @@ -142,14 +140,17 @@ public void renderProgressBar(Bounds barBounds, float fillFactor, int foreground blitTexture(sprites.progressBarBackground(), barBounds); - GlState.scissorTest(true); - scissorBox( + if (fillFactor == 0) { + return; + } + + this.enableScissor( (int) barBounds.left(), (int) barBounds.top(), (int) (barBounds.width() * fillFactor), (int) barBounds.height()); blitTexture(sprites.progressBarForeground(), barBounds, foregroundColor); - GlState.scissorTest(false); + this.collector.disableScissor(); } public void fillRect(float x, float y, float width, float height, int color) { @@ -157,20 +158,22 @@ public void fillRect(float x, float y, float width, float height, int color) { } public void fillRect(float x, float y, float width, float height, int colorTop, int colorBottom) { - bindShader("color"); - sharedBuffer.begin(SimpleBufferBuilder.Format.POS_TEX_COLOR, SimpleBufferBuilder.Mode.QUADS); + ELSRenderPipeline pipeline = this.pipelines.get(Theme.SHADER_COLOR); + sharedBuffer.begin(pipeline.vertexFormat(), pipeline.vertexMode()); sharedBuffer.pos(x, y).tex(0, 0).colour(colorTop).endVertex(); - sharedBuffer.pos(x + width, y).tex(0, 0).colour(colorTop).endVertex(); sharedBuffer.pos(x, y + height).tex(0, 0).colour(colorBottom).endVertex(); sharedBuffer.pos(x + width, y + height).tex(0, 0).colour(colorBottom).endVertex(); - sharedBuffer.draw(); + sharedBuffer.pos(x + width, y).tex(0, 0).colour(colorTop).endVertex(); + + SimpleBufferBuilder.Result result = this.sharedBuffer.finishAndUpload(this.backend); + this.collector.submitDraw(pipeline, null, result); } - public void scissorBox(int x, int y, int width, int height) { + public void enableScissor(int x, int y, int width, int height) { // glScissor applies to the whole window, not just the viewport set via glViewport - GlState.scissorBox( + this.collector.enableScissor( (int) (viewportOffsetX + x * viewportScale), - (int) (viewportOffsetY + y * viewportScale), + (int) (windowHeight - viewportOffsetY - (y + height) * viewportScale), (int) (width * viewportScale), (int) (height * viewportScale)); } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleBufferBuilder.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleBufferBuilder.java index 82ee95f22..31c9f0b7d 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleBufferBuilder.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleBufferBuilder.java @@ -5,37 +5,15 @@ package net.neoforged.fml.earlydisplay.render; -import static org.lwjgl.opengl.GL20C.glEnableVertexAttribArray; -import static org.lwjgl.opengl.GL32C.GL_ARRAY_BUFFER; -import static org.lwjgl.opengl.GL32C.GL_COPY_READ_BUFFER; -import static org.lwjgl.opengl.GL32C.GL_DYNAMIC_DRAW; -import static org.lwjgl.opengl.GL32C.GL_ELEMENT_ARRAY_BUFFER; -import static org.lwjgl.opengl.GL32C.GL_FLOAT; -import static org.lwjgl.opengl.GL32C.GL_MAP_INVALIDATE_BUFFER_BIT; -import static org.lwjgl.opengl.GL32C.GL_MAP_UNSYNCHRONIZED_BIT; -import static org.lwjgl.opengl.GL32C.GL_MAP_WRITE_BIT; -import static org.lwjgl.opengl.GL32C.GL_STATIC_DRAW; -import static org.lwjgl.opengl.GL32C.GL_TRIANGLES; -import static org.lwjgl.opengl.GL32C.GL_UNSIGNED_BYTE; -import static org.lwjgl.opengl.GL32C.GL_UNSIGNED_INT; -import static org.lwjgl.opengl.GL32C.glBindBuffer; -import static org.lwjgl.opengl.GL32C.glBufferData; -import static org.lwjgl.opengl.GL32C.glBufferSubData; -import static org.lwjgl.opengl.GL32C.glCopyBufferSubData; -import static org.lwjgl.opengl.GL32C.glDeleteBuffers; -import static org.lwjgl.opengl.GL32C.glDeleteVertexArrays; -import static org.lwjgl.opengl.GL32C.glDrawArrays; -import static org.lwjgl.opengl.GL32C.glDrawElements; -import static org.lwjgl.opengl.GL32C.glGenBuffers; -import static org.lwjgl.opengl.GL32C.glGenVertexArrays; -import static org.lwjgl.opengl.GL32C.glMapBufferRange; -import static org.lwjgl.opengl.GL32C.glUnmapBuffer; -import static org.lwjgl.opengl.GL32C.glVertexAttribPointer; - import java.io.Closeable; import java.nio.ByteBuffer; -import java.util.Arrays; +import java.util.EnumSet; +import java.util.Set; +import net.neoforged.fml.earlydisplay.render.backend.ELSBuffer; +import net.neoforged.fml.earlydisplay.render.backend.ELSRenderBackend; +import net.neoforged.fml.earlydisplay.render.backend.VertexFormat; import net.neoforged.fml.earlydisplay.theme.ThemeColor; +import org.jetbrains.annotations.Nullable; import org.lwjgl.system.MemoryUtil; /** @@ -44,42 +22,29 @@ *

* Not bound to any specific format, ideally should be held onto for re-use. *

- * Can be used for 'immediate mode' style rendering using {@link #draw()}, or - * upload to external vertex arrays for proper instancing using {@link #finishAndUpload()}. - *

* This is a Triangles only buffer, all data uploaded is in Triangles. * Quads are converted to triangles using {@code 0, 1, 2, 0, 2, 3}. *

- * Any given {@link Format} should have its individual {@link Element} components - * buffered in the order specified by the {@link Format}, + * Any given {@link VertexFormat} should have its individual {@link VertexFormat.Element} components + * buffered in the order specified by the {@link VertexFormat}, * followed by an {@link #endVertex()} call to prepare for the next vertex. *

* It is illegal to buffer primitives in any format other than the one specified to - * {@link #begin(Format, Mode)}. + * {@link #begin(VertexFormat, VertexFormat.Mode)}. * * @author covers1624 */ public class SimpleBufferBuilder implements Closeable { private static final MemoryUtil.MemoryAllocator ALLOCATOR = MemoryUtil.getAllocator(false); - - private static final int[] VERTEX_ARRAYS = new int[Format.values().length]; - private static final int[] VERTEX_BUFFERS = new int[Format.values().length]; - private static final int[] VERTEX_BUFFER_LENGTHS = new int[Format.values().length]; - private static int elementBuffer = 0; - private static int elementBufferVertexLength = 0; - - static { - Arrays.fill(VERTEX_ARRAYS, 0); - Arrays.fill(VERTEX_BUFFERS, 0); - Arrays.fill(VERTEX_BUFFER_LENGTHS, 0); - } + private static final Set BUFFER_USAGE = EnumSet.of(ELSBuffer.Usage.VERTEX, ELSBuffer.Usage.COPY_SRC, ELSBuffer.Usage.COPY_DST); private final String label; private long bufferAddr; // Pointer to the backing buffer. private ByteBuffer buffer; // ByteBuffer view of the backing buffer. - - private Format format; // The current format we are buffering. - private Mode mode; // The current mode we are buffering. + private ELSBuffer gpuBuffer; // GPU-side buffer + private long gpuBufferOffset; + private VertexFormat format; // The current format we are buffering. + private VertexFormat.Mode mode; // The current mode we are buffering. private boolean building; // If we are building the buffer. private int elementIndex; // The current element index we are buffering. if elementIndex == format.types.length, we expect 'endVertex' private int index; // The current index into the buffer we are writing to. @@ -101,75 +66,6 @@ public SimpleBufferBuilder(String label, int capacity) { buffer = MemoryUtil.memByteBuffer(bufferAddr, capacity); } - public static void destroy() { - glDeleteBuffers(VERTEX_BUFFERS); - glDeleteBuffers(elementBuffer); - glDeleteVertexArrays(VERTEX_ARRAYS); - - // Clear buffer IDs and lengths to allow re-initialization - Arrays.fill(VERTEX_ARRAYS, 0); - Arrays.fill(VERTEX_BUFFERS, 0); - Arrays.fill(VERTEX_BUFFER_LENGTHS, 0); - elementBuffer = 0; - elementBufferVertexLength = 0; - } - - private static void ensureElementBufferLength(int vertices) { - if (elementBufferVertexLength >= vertices) { - return; - } - - // treating it as immutable storage, even though it's not - var newElementBuffer = glGenBuffers(); - var newElementBufferVertexLength = Math.max(1024, elementBufferVertexLength); - while (newElementBufferVertexLength < vertices) { - newElementBufferVertexLength *= 2; - } - - var oldIndexCount = elementBufferVertexLength + elementBufferVertexLength / 2; - var newIndexCount = newElementBufferVertexLength + newElementBufferVertexLength / 2; - - // allocate new buffer - GlState.bindElementArrayBuffer(newElementBuffer); - GlDebug.labelBuffer(newElementBuffer, "EarlyDisplay shared index buffer"); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, newIndexCount * 4L, GL_STATIC_DRAW); - - // mapping avoids creating additional CPU copies of the data - // unsynchronized is fine because this is a brand-new buffer, and the old contents will be copied in afterward - // also can invalidate the whole buffer too, similarly because brand new, don't care what was there before - var mappingOffset = oldIndexCount * 4; - var mappingSize = (newIndexCount - oldIndexCount) * 4; - var mappedBuffer = glMapBufferRange(GL_ELEMENT_ARRAY_BUFFER, mappingOffset, mappingSize, GL_MAP_WRITE_BIT | GL_MAP_UNSYNCHRONIZED_BIT | GL_MAP_INVALIDATE_BUFFER_BIT); - - if (mappedBuffer == null) { - throw new NullPointerException("OpenGL buffer mapping failed"); - } - - int quads = newElementBufferVertexLength / 4; - int oldQuads = elementBufferVertexLength / 4; - // generate indices for the extension to the buffer - for (int i = oldQuads; i < quads; i++) { - // Quads are a bit different, we need to emit 2 triangles such that - // when combined they make up a single quad. - mappedBuffer.putInt(i * 4 + 0).putInt(i * 4 + 1).putInt(i * 4 + 2); - mappedBuffer.putInt(i * 4 + 1).putInt(i * 4 + 3).putInt(i * 4 + 2); - } - - glUnmapBuffer(GL_ELEMENT_ARRAY_BUFFER); - - if (elementBuffer != 0) { - // copy old data from previous element buffer - glBindBuffer(GL_COPY_READ_BUFFER, elementBuffer); - glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_ELEMENT_ARRAY_BUFFER, 0, 0, mappingOffset); - glBindBuffer(GL_COPY_READ_BUFFER, 0); - } - GlState.bindElementArrayBuffer(0); - - glDeleteBuffers(elementBuffer); - elementBuffer = newElementBuffer; - elementBufferVertexLength = newElementBufferVertexLength; - } - /** * Start building a new set of vertex data in the * given format and mode. @@ -177,7 +73,7 @@ private static void ensureElementBufferLength(int vertices) { * @param format The format to start building in. * @param mode The mode to start building in. */ - public SimpleBufferBuilder begin(Format format, Mode mode) { + public SimpleBufferBuilder begin(VertexFormat format, VertexFormat.Mode mode) { if (bufferAddr == MemoryUtil.NULL) { throw new IllegalStateException("Buffer has been freed."); // You already free'd the buffer } @@ -203,17 +99,22 @@ public SimpleBufferBuilder begin(Format format, Mode mode) { * @return The same builder. */ public SimpleBufferBuilder pos(float x, float y) { - if (!building) throw new IllegalStateException("Not building."); // You did not call begin. - - if (elementIndex == format.types.length) throw new IllegalStateException("Expected endVertex"); // we have reached the end of elements to buffer for this vertex, we expected an endVertex call. - if (format.types[elementIndex] != Element.POS) throw new IllegalArgumentException("Expected " + format.types[elementIndex]); // You called the wrong method for the format order. + if (!building) { + throw new IllegalStateException("Not building."); // You did not call begin. + } + if (elementIndex == format.elementCount()) { + throw new IllegalStateException("Expected endVertex"); // we have reached the end of elements to buffer for this vertex, we expected an endVertex call. + } + if (format.element(elementIndex) != VertexFormat.Element.POS) { + throw new IllegalArgumentException("Expected " + format.element(elementIndex)); // You called the wrong method for the format order. + } // Assumes that our POS element specifies the FLOAT data type. buffer.putFloat(index + 0, x); buffer.putFloat(index + 4, y); // Increment index for the number of bytes we wrote and increment the element index. - index += format.types[elementIndex].width; + index += format.element(elementIndex).width; elementIndex++; return this; } @@ -226,17 +127,22 @@ public SimpleBufferBuilder pos(float x, float y) { * @return The same builder. */ public SimpleBufferBuilder tex(float u, float v) { - if (!building) throw new IllegalStateException("Not building."); // You did not call begin. - - if (elementIndex == format.types.length) throw new IllegalStateException("Expected endVertex"); // we have reached the end of elements to buffer for this vertex, we expected an endVertex call. - if (format.types[elementIndex] != Element.TEX) throw new IllegalArgumentException("Expected " + format.types[elementIndex]); // You called the wrong method for the format order. + if (!building) { + throw new IllegalStateException("Not building."); // You did not call begin. + } + if (elementIndex == format.elementCount()) { + throw new IllegalStateException("Expected endVertex"); // we have reached the end of elements to buffer for this vertex, we expected an endVertex call. + } + if (format.element(elementIndex) != VertexFormat.Element.TEX) { + throw new IllegalArgumentException("Expected " + format.element(elementIndex)); // You called the wrong method for the format order. + } // Assumes our TEX element specifies the FLOAT data type. buffer.putFloat(index + 0, u); buffer.putFloat(index + 4, v); // Increment index for the number of bytes we wrote and increment the element index. - index += format.types[elementIndex].width; + index += format.element(elementIndex).width; elementIndex++; return this; } @@ -275,10 +181,15 @@ public SimpleBufferBuilder colour(int packedColor) { * @return The same buffer. */ public SimpleBufferBuilder colour(byte r, byte g, byte b, byte a) { - if (!building) throw new IllegalStateException("Not building."); // You did not call begin. - - if (elementIndex == format.types.length) throw new IllegalStateException("Expected endVertex"); // we have reached the end of elements to buffer for this vertex, we expected an endVertex call. - if (format.types[elementIndex] != Element.COLOR) throw new IllegalArgumentException("Expected " + format.types[elementIndex]); // You called the wrong method for the format order. + if (!building) { + throw new IllegalStateException("Not building."); // You did not call begin. + } + if (elementIndex == format.elementCount()) { + throw new IllegalStateException("Expected endVertex"); // we have reached the end of elements to buffer for this vertex, we expected an endVertex call. + } + if (format.element(elementIndex) != VertexFormat.Element.COLOR) { + throw new IllegalArgumentException("Expected " + format.element(elementIndex)); // You called the wrong method for the format order. + } // Assumes our COLOR element specifies the UNSIGNED_BYTE data type. buffer.put(index + 0, r); @@ -287,7 +198,7 @@ public SimpleBufferBuilder colour(byte r, byte g, byte b, byte a) { buffer.put(index + 3, a); // Increment index for the number of bytes we wrote and increment the element index. - index += format.types[elementIndex].width; + index += format.element(elementIndex).width; elementIndex++; return this; } @@ -298,9 +209,12 @@ public SimpleBufferBuilder colour(byte r, byte g, byte b, byte a) { * @return The same builder. */ public SimpleBufferBuilder endVertex() { - if (!building) throw new IllegalStateException("Not building."); // You did not call begin. - - if (elementIndex != format.types.length) throw new IllegalStateException("Expected " + format.types[elementIndex]); // You did not finish building the vertex. + if (!building) { + throw new IllegalStateException("Not building."); // You did not call begin. + } + if (elementIndex != format.elementCount()) { + throw new IllegalStateException("Expected " + format.element(elementIndex)); // You did not finish building the vertex. + } // Reset elementIndex elementIndex = 0; @@ -332,51 +246,58 @@ private void ensureSpace(int newBytes) { *

* Uploading the buffers finishes drawing and resets for the next buffer operation. *

- * This should not be called in conjunction with {@link #draw()} * * @return The number of indexes that were uploaded. */ - public int finishAndUpload() { - if (!building) throw new IllegalStateException("Not building."); + @Nullable + public Result finishAndUpload(ELSRenderBackend backend) { + if (!building) { + throw new IllegalStateException("Not building."); + } - int indices; try { - if (elementIndex == format.types.length) throw new IllegalStateException("Expected endVertex"); // You didn't finish building your vertex. - if (elementIndex != 0) throw new IllegalStateException("Not finished building vertex, Expected: " + format.types[elementIndex]); // You didn't finish building your vertex data. - if (vertices == 0) return 0; // No vertices buffered, lets not do anything. - if (vertices % mode.vertices != 0) throw new IllegalStateException("Does not contain vertices aligned to " + mode); // You did not put in enough vertices to cleanly slice the data into TRIANGLES/QUADS + if (elementIndex == format.elementCount()) { + throw new IllegalStateException("Expected endVertex"); // You didn't finish building your vertex. + } + if (elementIndex != 0) { + throw new IllegalStateException("Not finished building vertex, Expected: " + format.element(elementIndex)); // You didn't finish building your vertex data. + } + if (vertices == 0) { + return null; // No vertices buffered, lets not do anything. + } + if (vertices % mode.vertices != 0) { + throw new IllegalStateException("Does not contain vertices aligned to " + mode); // You did not put in enough vertices to cleanly slice the data into TRIANGLES/QUADS + } // Reset position to 0, limit the buffer to our index. buffer.position(0); buffer.limit(index); // Upload the raw vertex data in dynamic mode. - int vbo = VERTEX_BUFFERS[format.ordinal()]; - int vboSize = VERTEX_BUFFER_LENGTHS[format.ordinal()]; - GlState.bindArrayBuffer(vbo); - if (vboSize < index) { + long bufferSize = this.gpuBufferOffset + this.index; + if (this.gpuBuffer == null || this.gpuBuffer.size() < bufferSize) { // expand buffer, it's not big enough - var newVBOSize = Math.max(1024, vboSize); - while (newVBOSize < index) { + long newVBOSize = Math.max(1024, this.gpuBuffer != null ? this.gpuBuffer.size() : 0); + while (newVBOSize < bufferSize) { newVBOSize *= 2; } - // because everything is overwritten anyway, we can do an in-place reallocation - glBufferData(GL_ARRAY_BUFFER, newVBOSize, GL_DYNAMIC_DRAW); - VERTEX_BUFFER_LENGTHS[format.ordinal()] = newVBOSize; + ELSBuffer oldBuffer = this.gpuBuffer; + this.gpuBuffer = backend.createBuffer(this.label, BUFFER_USAGE, newVBOSize); + if (oldBuffer != null) { + backend.copyBufferToBuffer(oldBuffer.slice(), this.gpuBuffer.slice(0, oldBuffer.size())); + oldBuffer.close(); + } } - glBufferSubData(GL_ARRAY_BUFFER, 0, buffer); + backend.writeToBuffer(this.gpuBuffer.slice(this.gpuBufferOffset, this.index), this.buffer); + long resultOffset = this.gpuBufferOffset; + this.gpuBufferOffset += this.index; // The number of indices for triangles is equal to our vertex count, as that is // what we operate in. However, for Quads, we have exactly vertices + vertices / 2 // vertices once we convert the quads to triangles. - indices = mode == Mode.TRIANGLES ? vertices : vertices + vertices / 2; + int indices = mode == VertexFormat.Mode.TRIANGLES ? vertices : vertices + vertices / 2; - if (mode == Mode.QUADS) { - ensureElementBufferLength(vertices); - GlState.bindElementArrayBuffer(elementBuffer); - } - - return indices; + return new Result(this.format, resultOffset, this.vertices, indices, this.mode == VertexFormat.Mode.QUADS); } finally { // Reset builder state for next begin call. building = false; @@ -385,61 +306,16 @@ public int finishAndUpload() { } } - /** - * Upload and draw this buffer using one of a number of re-usable set of buffers. - *

- * This will immediately upload the buffer, resetting this builder for the next - * buffer operation, and draw the uploaded data. - *

- * You will need to bind shaders, textures, etc, before calling this function. - */ - public void draw() { - if (!building) throw new IllegalStateException("Not building."); - - int vao = VERTEX_ARRAYS[format.ordinal()]; - int vbo = VERTEX_BUFFERS[format.ordinal()]; - - if (vao == 0) { - // These 3 buffers are paired, you can't allocate one without the others. - assert vbo == 0; - - // Make new vertex array and buffers! - vao = glGenVertexArrays(); - vbo = glGenBuffers(); - - // Cache the vertex array and buffers for future re-use. - VERTEX_ARRAYS[format.ordinal()] = vao; - VERTEX_BUFFERS[format.ordinal()] = vbo; - - // Ask our Format to set up its data layout for the vertex array. - // but only once, the VAO saves this state - GlState.bindVertexArray(vao); - GlState.bindArrayBuffer(vbo); - GlDebug.labelVertexArray(vao, label); - GlDebug.labelBuffer(vbo, label); - format.bind(); - format.enable(); - } - // Bind the vertex array and buffers! - GlState.bindVertexArray(vao); - - // Upload the data. - int indices = finishAndUpload(); - - if (mode == Mode.QUADS) { - glDrawElements(GL_TRIANGLES, indices, GL_UNSIGNED_INT, 0); - } else { - glDrawArrays(GL_TRIANGLES, 0, indices); - } + public void endFrame() { + this.gpuBufferOffset = 0L; + } - // Unbind the vertex array. - GlState.bindVertexArray(0); + public ELSBuffer getGpuBuffer() { + return this.gpuBuffer; } /** * Clear this builder's cached buffer. - *

- * If you are completely done, call {@link #destroy()} */ @Override public void close() { @@ -447,88 +323,5 @@ public void close() { bufferAddr = MemoryUtil.NULL; } - /** - * Represents a primitive mode that this builder is capable of buffering in. - */ - public enum Mode { - TRIANGLES(3), - QUADS(4), - ; - - public final int vertices; - - Mode(int vertices) { - this.vertices = vertices; - } - } - - /** - * Specifies a vertex element with a specific data type, number of primitives and a size in bytes. - */ - public enum Element { - POS(GL_FLOAT, 2, 2 * 4), - TEX(GL_FLOAT, 2, 2 * 4), - COLOR(GL_UNSIGNED_BYTE, 4, 4); - - public final int glType; - public final int count; - public final int width; - - Element(int glType, int count, int width) { - this.glType = glType; - this.count = count; - this.width = width; - } - } - - /** - * Specifies a combination of vertex elements. - */ - public enum Format { - POS(Element.POS), - POS_TEX(Element.POS, Element.TEX), - POS_COLOR(Element.POS, Element.COLOR), - POS_TEX_COLOR(Element.POS, Element.TEX, Element.COLOR); - - private final Element[] types; - public final int stride; - - Format(Element... types) { - this.types = types; - - // Stride is the width of each vertex in bytes. - stride = Arrays.stream(types).mapToInt(e -> e.width).sum(); - } - - /** - * set up the attribute pointers for this format. - *

- * Assumes that an array buffer is already bound and ready to go. - */ - public void bind() { - int offset = 0; - - // Set up the pointers that tell GL where our interleaved - // vertex data is in the buffers. - for (int i = 0; i < types.length; i++) { - Element type = types[i]; - switch (type.glType) { - case GL_FLOAT -> glVertexAttribPointer(i, type.count, GL_FLOAT, false, stride, offset); - case GL_UNSIGNED_BYTE -> glVertexAttribPointer(i, type.count, GL_UNSIGNED_BYTE, true, stride, offset); - default -> throw new IllegalStateException("Unknown glType, I don't know how to bind this vertex element: " + type); - } - // add to the offset for the next element. - offset += type.width; - } - } - - /** - * Enables the vertex attributes this format contains. - */ - public void enable() { - for (int i = 0; i < types.length; i++) { - glEnableVertexAttribArray(i); - } - } - } + public record Result(VertexFormat format, long vertexOffset, int vertexCount, int indexCount, boolean indexed) {} } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleFont.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleFont.java index 3da96d334..5f58245b6 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleFont.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleFont.java @@ -5,8 +5,6 @@ package net.neoforged.fml.earlydisplay.render; -import static org.lwjgl.opengl.GL30C.GL_R8; -import static org.lwjgl.opengl.GL32C.GL_RED; import static org.lwjgl.stb.STBTruetype.stbtt_GetPackedQuad; import static org.lwjgl.stb.STBTruetype.stbtt_GetScaledFontVMetrics; import static org.lwjgl.stb.STBTruetype.stbtt_InitFont; @@ -22,11 +20,13 @@ import java.util.List; import java.util.Objects; import java.util.function.IntFunction; +import net.neoforged.fml.earlydisplay.render.backend.ELSRenderBackend; +import net.neoforged.fml.earlydisplay.render.backend.ELSTexture; +import net.neoforged.fml.earlydisplay.render.backend.TextureFormat; import net.neoforged.fml.earlydisplay.theme.ThemeResource; import net.neoforged.fml.earlydisplay.util.Size; import org.jetbrains.annotations.Nullable; import org.lwjgl.BufferUtils; -import org.lwjgl.opengl.GL32C; import org.lwjgl.stb.STBTTAlignedQuad; import org.lwjgl.stb.STBTTFontinfo; import org.lwjgl.stb.STBTTPackContext; @@ -36,7 +36,7 @@ public class SimpleFont implements AutoCloseable { private static final int ASCII_GLYPH_COUNT = 127 - 32; - private int textureId; + private final ELSTexture texture; private final int lineSpacing; private final int descent; private final IntFunction glyphGetter; @@ -77,10 +77,7 @@ public Size measureText(CharSequence text) { @Override public void close() { - if (textureId != 0) { - GL32C.glDeleteTextures(textureId); - textureId = 0; - } + this.texture.close(); } public record Glyph(char c, int charwidth, int[] pos, float[] uv) { @@ -90,24 +87,24 @@ Pos loadQuad(Pos pos, int colour, SimpleBufferBuilder bb) { var x1 = pos.x() + pos()[2]; var y1 = pos.y() + pos()[3]; bb.pos(x0, y0).tex(uv()[0], uv()[1]).colour(colour).endVertex(); - bb.pos(x1, y0).tex(uv()[2], uv()[1]).colour(colour).endVertex(); bb.pos(x0, y1).tex(uv()[0], uv()[3]).colour(colour).endVertex(); bb.pos(x1, y1).tex(uv()[2], uv()[3]).colour(colour).endVertex(); + bb.pos(x1, y0).tex(uv()[2], uv()[1]).colour(colour).endVertex(); return new Pos(pos.x() + charwidth(), pos.y(), pos.minx()); } } - public SimpleFont(int lineSpacing, int descent, int textureId, IntFunction glyphGetter) { + public SimpleFont(int lineSpacing, int descent, ELSTexture texture, IntFunction glyphGetter) { this.lineSpacing = lineSpacing; this.descent = descent; - this.textureId = textureId; + this.texture = texture; this.glyphGetter = glyphGetter; } /** * Build the font and store it in the textureNumber location */ - public SimpleFont(ThemeResource resource, @Nullable Path externalThemeDirectory) throws IOException { + public SimpleFont(ELSRenderBackend backend, ThemeResource resource, @Nullable Path externalThemeDirectory) throws IOException { try (var nativeBuffer = resource.toNativeBuffer(externalThemeDirectory)) { var buf = nativeBuffer.buffer(); var info = STBTTFontinfo.create(); @@ -124,7 +121,7 @@ public SimpleFont(ThemeResource resource, @Nullable Path externalThemeDirectory) this.descent = (int) Math.floor(descent[0]); int texwidth = 256; int texheight = 128; - this.textureId = Texture.createEmpty("font texture " + resource, texwidth, texheight, GL_R8, GL_RED, false); + this.texture = backend.createTexture("font texture " + resource, texwidth, texheight, TextureFormat.RED, false); try (var packedchars = STBTTPackedchar.malloc(ASCII_GLYPH_COUNT)) { try (STBTTPackRange.Buffer packRanges = STBTTPackRange.malloc(1)) { var bitmap = BufferUtils.createByteBuffer(texwidth * texheight); @@ -139,7 +136,7 @@ public SimpleFont(ThemeResource resource, @Nullable Path externalThemeDirectory) stbtt_PackSetSkipMissingCodepoints(pc, true); stbtt_PackFontRanges(pc, buf, 0, packRanges); stbtt_PackEnd(pc); - Texture.writeToTexture(this.textureId, texwidth, texheight, GL_RED, 1, bitmap); + backend.writeToTexture(this.texture, bitmap); } } try (var q = STBTTAlignedQuad.malloc()) { @@ -178,8 +175,8 @@ public int lineSpacing() { return lineSpacing; } - int textureId() { - return textureId; + ELSTexture texture() { + return this.texture; } public int descent() { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/Texture.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/Texture.java index 02e0f30f8..400185407 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/Texture.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/Texture.java @@ -5,39 +5,17 @@ package net.neoforged.fml.earlydisplay.render; -import static org.lwjgl.opengl.GL11C.GL_LINEAR; -import static org.lwjgl.opengl.GL11C.GL_NEAREST; -import static org.lwjgl.opengl.GL11C.GL_RGBA; -import static org.lwjgl.opengl.GL11C.GL_RGBA8; -import static org.lwjgl.opengl.GL11C.GL_TEXTURE_2D; -import static org.lwjgl.opengl.GL11C.GL_TEXTURE_MAG_FILTER; -import static org.lwjgl.opengl.GL11C.GL_TEXTURE_MIN_FILTER; -import static org.lwjgl.opengl.GL11C.GL_TEXTURE_WRAP_S; -import static org.lwjgl.opengl.GL11C.GL_TEXTURE_WRAP_T; -import static org.lwjgl.opengl.GL11C.GL_UNPACK_ALIGNMENT; -import static org.lwjgl.opengl.GL11C.GL_UNPACK_ROW_LENGTH; -import static org.lwjgl.opengl.GL11C.GL_UNPACK_SKIP_PIXELS; -import static org.lwjgl.opengl.GL11C.GL_UNPACK_SKIP_ROWS; -import static org.lwjgl.opengl.GL11C.GL_UNSIGNED_BYTE; -import static org.lwjgl.opengl.GL11C.glGenTextures; -import static org.lwjgl.opengl.GL11C.glPixelStorei; -import static org.lwjgl.opengl.GL11C.glTexImage2D; -import static org.lwjgl.opengl.GL11C.glTexParameteri; -import static org.lwjgl.opengl.GL11C.glTexSubImage2D; -import static org.lwjgl.opengl.GL12C.GL_CLAMP_TO_EDGE; - -import java.nio.ByteBuffer; import java.nio.file.Path; +import net.neoforged.fml.earlydisplay.render.backend.ELSRenderBackend; +import net.neoforged.fml.earlydisplay.render.backend.ELSTexture; +import net.neoforged.fml.earlydisplay.render.backend.TextureFormat; import net.neoforged.fml.earlydisplay.theme.AnimationMetadata; import net.neoforged.fml.earlydisplay.theme.TextureScaling; import net.neoforged.fml.earlydisplay.theme.ThemeTexture; import net.neoforged.fml.earlydisplay.theme.UncompressedImage; import org.jetbrains.annotations.Nullable; -import org.lwjgl.opengl.GL32C; -public record Texture(int textureId, int physicalWidth, int physicalHeight, - TextureScaling scaling, - @Nullable AnimationMetadata animationMetadata) implements AutoCloseable { +public record Texture(ELSTexture texture, TextureScaling scaling, @Nullable AnimationMetadata animationMetadata) implements AutoCloseable { public int width() { return scaling.width(); } @@ -46,12 +24,20 @@ public int height() { return scaling.height(); } + public int physicalWidth() { + return this.texture.width(); + } + + public int physicalHeight() { + return this.texture.height(); + } + /** * Loads a resource into an OpenGL texture. */ - public static Texture create(ThemeTexture themeTexture, @Nullable Path externalThemeDirectory) { + public static Texture create(ELSRenderBackend backend, ThemeTexture themeTexture, @Nullable Path externalThemeDirectory) { try (var image = themeTexture.resource().loadAsImage(externalThemeDirectory)) { - return create(image, "EarlyDisplay " + themeTexture, themeTexture.scaling(), themeTexture.animation()); + return create(backend, image, "EarlyDisplay " + themeTexture, themeTexture.scaling(), themeTexture.animation()); } } @@ -62,68 +48,18 @@ public static Texture create(ThemeTexture themeTexture, @Nullable Path externalT /// @param scaling The scaling to apply to the texture /// @param animation The animation, if any, to render the texture with public static Texture create( + ELSRenderBackend backend, UncompressedImage image, String debugName, TextureScaling scaling, @Nullable AnimationMetadata animation) { - // Initializing the texture (via glTexImage2D() with a null buffer) and writing its contents (via glTexSubImage2D()) - // has to happen separately as doing both in one glTexImage2D() call may cause segfaults in some cases, - // particularly if invoked after vanilla has initialized its renderer and started creating its own textures. - int texId = createEmpty(debugName, image.width(), image.height(), GL_RGBA8, GL_RGBA, scaling.linearScaling()); - writeToTexture(texId, image.width(), image.height(), GL_RGBA, 4, image.imageData()); - return new Texture(texId, image.width(), image.height(), scaling, animation); - } - - /// Create an empty GL texture with the specified parameters. - /// - /// @param width The width of the texture in pixels - /// @param height The height of the texture in pixels - /// @param internalFormat The internal GL format - /// @param externalFormat The external GL format - /// @param linearFilter Whether the texture should use linear or nearest-neighbor filtering - public static int createEmpty( - String debugName, - int width, - int height, - int internalFormat, - int externalFormat, - boolean linearFilter) { - int texId = glGenTextures(); - GlState.bindTexture2D(texId); - GlDebug.labelTexture(texId, debugName); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, linearFilter ? GL_LINEAR : GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, linearFilter ? GL_LINEAR : GL_NEAREST); - glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width, height, 0, externalFormat, GL_UNSIGNED_BYTE, (ByteBuffer) null); - return texId; - } - - /// Write the provided pixel buffer's contents to the specified texture. - /// - /// @param textureId The GL ID of the target texture - /// @param width The width of the texture in pixels - /// @param height The height of the texture in pixels - /// @param externalFormat The external GL format - /// @param components The amount of components per pixel - /// @param pixels The pixel data to write to the texture - public static void writeToTexture( - int textureId, - int width, - int height, - int externalFormat, - int components, - ByteBuffer pixels) { - GlState.bindTexture2D(textureId); - glPixelStorei(GL_UNPACK_ROW_LENGTH, width); - glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0); - glPixelStorei(GL_UNPACK_SKIP_ROWS, 0); - glPixelStorei(GL_UNPACK_ALIGNMENT, components); - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, externalFormat, GL_UNSIGNED_BYTE, pixels); + ELSTexture texture = backend.createTexture(debugName, image.width(), image.height(), TextureFormat.RGBA, scaling.linearScaling()); + backend.writeToTexture(texture, image.imageData()); + return new Texture(texture, scaling, animation); } @Override public void close() { - GL32C.glDeleteTextures(textureId); + this.texture.close(); } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/ELSBuffer.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/ELSBuffer.java new file mode 100644 index 000000000..aa7081553 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/ELSBuffer.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.earlydisplay.render.backend; + +import java.util.Set; + +public interface ELSBuffer extends AutoCloseable { + Set usage(); + + long size(); + + ELSBufferSlice slice(); + + ELSBufferSlice slice(long offset, long length); + + @Override + void close(); + + enum Usage { + MAP_READ, + MAP_WRITE, + HINT_CLIENT_STORAGE, + COPY_DST, + COPY_SRC, + VERTEX, + INDEX, + UNIFORM, + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/ELSBufferSlice.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/ELSBufferSlice.java new file mode 100644 index 000000000..ba870c3ea --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/ELSBufferSlice.java @@ -0,0 +1,14 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.earlydisplay.render.backend; + +public interface ELSBufferSlice { + ELSBuffer buffer(); + + long offset(); + + long length(); +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/ELSDrawCollector.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/ELSDrawCollector.java new file mode 100644 index 000000000..e358c4b53 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/ELSDrawCollector.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.earlydisplay.render.backend; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import net.neoforged.fml.earlydisplay.render.ElementShader; +import net.neoforged.fml.earlydisplay.render.SimpleBufferBuilder; +import net.neoforged.fml.earlydisplay.theme.ThemeColor; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.system.MemoryUtil; + +public final class ELSDrawCollector { + private final ELSRenderBackend backend; + private final List draws = new ArrayList<>(); + private int viewportX; + private int viewportY; + private int viewportWidth; + private int viewportHeight; + private boolean scissorEnabled; + private int scissorX; + private int scissorY; + private int scissorWidth; + private int scissorHeight; + + public ELSDrawCollector(ELSRenderBackend backend) { + this.backend = backend; + } + + public void setViewport(int x, int y, int width, int height) { + this.viewportX = x; + this.viewportY = y; + this.viewportWidth = width; + this.viewportHeight = height; + } + + public void enableScissor(int x, int y, int width, int height) { + if (width <= 0 || height <= 0) { + throw new IllegalArgumentException("Stencil area must have non-zero width/height"); + } + + this.scissorEnabled = true; + this.scissorX = x; + this.scissorY = y; + this.scissorWidth = width; + this.scissorHeight = height; + } + + public void disableScissor() { + this.scissorEnabled = false; + } + + public void submitDraw(ELSRenderPipeline pipeline, @Nullable ELSTexture texture, @Nullable SimpleBufferBuilder.Result bufferResult) { + if (bufferResult != null) { + this.draws.add(new Draw(pipeline, texture, bufferResult, this.scissorEnabled, this.scissorX, this.scissorY, this.scissorWidth, this.scissorHeight)); + } + } + + public void execute(String label, ELSTexture target, ThemeColor clearColor, ELSBuffer vertexBuffer, int screenWidth, int screenHeight) { + int maxIndices = this.draws.stream() + .filter(Draw::indexed) + .mapToInt(Draw::indexCount) + .max() + .orElse(0); + ELSBuffer indexBuffer = this.backend.getQuadAutoIndexBuffer(maxIndices); + + ByteBuffer uboData = MemoryUtil.memAlloc(2 * 4); + try { + uboData.putFloat(screenWidth); + uboData.putFloat(screenHeight); + uboData.rewind(); + this.backend.writeToBuffer(this.backend.screenSizeUbo.slice(), uboData); + } finally { + MemoryUtil.memFree(uboData); + } + + try (ELSRenderPass renderPass = this.backend.createRenderPass(label, target, clearColor)) { + renderPass.setViewport(this.viewportX, this.viewportY, this.viewportWidth, this.viewportHeight); + renderPass.bindUniform(ElementShader.UNIFORM_SCREEN_SIZE, this.backend.screenSizeUbo); + + for (Draw draw : this.draws) { + SimpleBufferBuilder.Result result = draw.bufferResult; + + renderPass.bindPipeline(draw.pipeline); + renderPass.bindVertexBuffer(vertexBuffer.slice(result.vertexOffset(), result.vertexCount() * (long) result.format().stride)); + renderPass.bindIndexBuffer(result.indexed() ? indexBuffer : null); + renderPass.bindTexture(ElementShader.UNIFORM_SAMPLER0, draw.texture); + + if (draw.scissorEnabled) { + renderPass.enableScissor(draw.scissorX, draw.scissorY, draw.scissorWidth, draw.scissorHeight); + } else { + renderPass.disableScissor(); + } + + if (result.indexed()) { + renderPass.drawIndexed(result.indexCount()); + } else { + renderPass.draw(result.vertexCount()); + } + } + } + } + + private record Draw( + ELSRenderPipeline pipeline, + @Nullable ELSTexture texture, + SimpleBufferBuilder.Result bufferResult, + boolean scissorEnabled, + int scissorX, + int scissorY, + int scissorWidth, + int scissorHeight) { + boolean indexed() { + return this.bufferResult.indexed(); + } + + int indexCount() { + return this.bufferResult.indexCount(); + } + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/ELSRenderBackend.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/ELSRenderBackend.java new file mode 100644 index 000000000..0b978904c --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/ELSRenderBackend.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.earlydisplay.render.backend; + +import java.nio.ByteBuffer; +import java.util.Collection; +import java.util.Set; +import net.neoforged.fml.earlydisplay.theme.ThemeColor; + +public abstract class ELSRenderBackend implements AutoCloseable { + final ELSBuffer screenSizeUbo; + + protected ELSRenderBackend() { + this.screenSizeUbo = this.createBuffer("ScreenSize UBO", Set.of(ELSBuffer.Usage.UNIFORM, ELSBuffer.Usage.COPY_DST), 2 * 4); + } + + public abstract void preloadPipelines(Collection pipelines); + + public abstract ELSTexture createTexture(String debugName, int width, int height, TextureFormat format, boolean linearFilter); + + public abstract void writeToTexture(ELSTexture texture, ByteBuffer pixels); + + public abstract ELSBuffer createBuffer(String label, Set usage, long size); + + public abstract ELSBuffer createBuffer(String label, Set usage, ByteBuffer data); + + public abstract void writeToBuffer(ELSBufferSlice buffer, ByteBuffer data); + + public abstract void copyBufferToBuffer(ELSBufferSlice source, ELSBufferSlice destination); + + public abstract ELSBuffer getQuadAutoIndexBuffer(int indexCount); + + public abstract ELSRenderPass createRenderPass(String label, ELSTexture target, ThemeColor clearColor); + + public abstract boolean startFrame(FramebufferSizeListener listener); + + public abstract void presentTexture(ELSTexture texture, ThemeColor backgroundColor, int windowFBWidth, int windowFBHeight); + + public abstract int getMaxTextureSize(); + + public abstract long getWindowHandle(); + + public abstract void acquireContextOwnership(boolean createContext); + + public abstract void releaseContextOwnership(); + + public abstract void guardResourceCleanup(Runnable cleanupTask); + + @Override + public abstract void close(); + + public abstract String name(); + + public interface FramebufferSizeListener { + void accept(int width, int height); + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/ELSRenderPass.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/ELSRenderPass.java new file mode 100644 index 000000000..0039e8a4b --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/ELSRenderPass.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.earlydisplay.render.backend; + +import org.jetbrains.annotations.Nullable; + +public interface ELSRenderPass extends AutoCloseable { + void setViewport(int x, int y, int width, int height); + + void enableScissor(int x, int y, int width, int height); + + void disableScissor(); + + void bindPipeline(ELSRenderPipeline pipeline); + + void bindTexture(String name, @Nullable ELSTexture texture); + + void bindUniform(String name, ELSBuffer buffer); + + void bindVertexBuffer(ELSBufferSlice buffer); + + void bindIndexBuffer(@Nullable ELSBuffer buffer); + + void draw(int vertexCount); + + void drawIndexed(int indexCount); + + @Override + void close(); +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/ELSRenderPipeline.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/ELSRenderPipeline.java new file mode 100644 index 000000000..2e578ca80 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/ELSRenderPipeline.java @@ -0,0 +1,17 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.earlydisplay.render.backend; + +import java.util.List; +import net.neoforged.fml.earlydisplay.render.ElementShader; +import org.jetbrains.annotations.Nullable; + +public record ELSRenderPipeline( + ElementShader shader, + VertexFormat vertexFormat, + VertexFormat.Mode vertexMode, + @Nullable String sampler, + List uniforms) {} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/ELSTexture.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/ELSTexture.java new file mode 100644 index 000000000..54a4f3076 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/ELSTexture.java @@ -0,0 +1,17 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.earlydisplay.render.backend; + +public interface ELSTexture extends AutoCloseable { + int width(); + + int height(); + + TextureFormat format(); + + @Override + void close(); +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/TextureFormat.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/TextureFormat.java new file mode 100644 index 000000000..5c8608e6f --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/TextureFormat.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.earlydisplay.render.backend; + +public enum TextureFormat { + RGBA(4), + RED(1), + ; + + private final int components; + + TextureFormat(int components) { + this.components = components; + } + + public int getComponents() { + return components; + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/VertexFormat.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/VertexFormat.java new file mode 100644 index 000000000..dfbb28841 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/VertexFormat.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.earlydisplay.render.backend; + +import java.util.Arrays; + +public enum VertexFormat { + POS(Element.POS), + POS_TEX(Element.POS, Element.TEX), + POS_COLOR(Element.POS, Element.COLOR), + POS_TEX_COLOR(Element.POS, Element.TEX, Element.COLOR); + + private final Element[] elements; + public final int stride; + + VertexFormat(Element... elements) { + this.elements = elements; + this.stride = Arrays.stream(elements).mapToInt(e -> e.width).sum(); + } + + public Element element(int idx) { + return this.elements[idx]; + } + + public int elementCount() { + return this.elements.length; + } + + public int findElement(Element target) { + for (int i = 0; i < this.elements.length; i++) { + if (this.elements[i] == target) { + return i; + } + } + return -1; + } + + public enum Element { + POS("position", 2, 2 * 4), + TEX("uv", 2, 2 * 4), + COLOR("color", 4, 4); + + public final String name; + public final int count; + public final int width; + + Element(String name, int count, int width) { + this.name = name; + this.count = count; + this.width = width; + } + } + + public enum Mode { + TRIANGLES(3), + QUADS(4), + ; + + public final int vertices; + + Mode(int vertices) { + this.vertices = vertices; + } + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/GlBuffer.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/GlBuffer.java new file mode 100644 index 000000000..de4368c18 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/GlBuffer.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.earlydisplay.render.backend.opengl; + +import java.util.Set; +import net.neoforged.fml.earlydisplay.render.backend.ELSBuffer; +import org.lwjgl.opengl.GL33C; + +final class GlBuffer implements ELSBuffer { + final int bufferId; + private final Set usage; + private final long size; + private final GlBufferSlice defaultSlice; + + GlBuffer(int bufferId, Set usage, long size) { + this.bufferId = bufferId; + this.usage = usage; + this.size = size; + this.defaultSlice = new GlBufferSlice(this, 0, size); + } + + @Override + public Set usage() { + return usage; + } + + @Override + public long size() { + return size; + } + + @Override + public GlBufferSlice slice() { + return this.defaultSlice; + } + + @Override + public GlBufferSlice slice(long offset, long length) { + return new GlBufferSlice(this, offset, length); + } + + @Override + public void close() { + GL33C.glDeleteBuffers(this.bufferId); + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/GlBufferSlice.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/GlBufferSlice.java new file mode 100644 index 000000000..d30f19ffd --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/GlBufferSlice.java @@ -0,0 +1,10 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.earlydisplay.render.backend.opengl; + +import net.neoforged.fml.earlydisplay.render.backend.ELSBufferSlice; + +record GlBufferSlice(GlBuffer buffer, long offset, long length) implements ELSBufferSlice {} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/GlCompiledPipeline.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/GlCompiledPipeline.java new file mode 100644 index 000000000..86e092a24 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/GlCompiledPipeline.java @@ -0,0 +1,10 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.earlydisplay.render.backend.opengl; + +import net.neoforged.fml.earlydisplay.render.backend.ELSRenderPipeline; + +record GlCompiledPipeline(ELSRenderPipeline info, GlProgram program) {} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/GlConst.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/GlConst.java new file mode 100644 index 000000000..19ed3652e --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/GlConst.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.earlydisplay.render.backend.opengl; + +import java.util.Set; +import net.neoforged.fml.earlydisplay.render.backend.ELSBuffer; +import net.neoforged.fml.earlydisplay.render.backend.TextureFormat; +import org.lwjgl.opengl.GL33C; + +final class GlConst { + static int toGlInternalId(TextureFormat format) { + return switch (format) { + case RGBA -> GL33C.GL_RGBA8; + case RED -> GL33C.GL_R8; + }; + } + + static int toGlExternalId(TextureFormat format) { + return switch (format) { + case RGBA -> GL33C.GL_RGBA; + case RED -> GL33C.GL_RED; + }; + } + + static int getBufferBindTarget(Set usage) { + if (usage.contains(ELSBuffer.Usage.VERTEX)) { + return GL33C.GL_ARRAY_BUFFER; + } + if (usage.contains(ELSBuffer.Usage.INDEX)) { + return GL33C.GL_ELEMENT_ARRAY_BUFFER; + } + if (usage.contains(ELSBuffer.Usage.UNIFORM)) { + return GL33C.GL_UNIFORM_BUFFER; + } + return GL33C.GL_COPY_WRITE_BUFFER; + } + + static int bufferUsageToGlEnum(Set usage) { + boolean clientStorage = usage.contains(ELSBuffer.Usage.HINT_CLIENT_STORAGE); + if (usage.contains(ELSBuffer.Usage.MAP_WRITE)) { + return clientStorage ? GL33C.GL_STREAM_DRAW : GL33C.GL_STATIC_DRAW; + } + if (usage.contains(ELSBuffer.Usage.MAP_READ)) { + return clientStorage ? GL33C.GL_STREAM_READ : GL33C.GL_STATIC_READ; + } + return GL33C.GL_STATIC_DRAW; + } + + private GlConst() {} +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/GlDebug.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/GlDebug.java similarity index 98% rename from earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/GlDebug.java rename to earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/GlDebug.java index 010cdf6f0..2064c51e8 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/GlDebug.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/GlDebug.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: LGPL-2.1-only */ -package net.neoforged.fml.earlydisplay.render; +package net.neoforged.fml.earlydisplay.render.backend.opengl; import net.neoforged.fml.loading.FMLConfig; import org.lwjgl.opengl.EXTDebugLabel; @@ -15,7 +15,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public final class GlDebug { +final class GlDebug { private static final Logger LOG = LoggerFactory.getLogger(GlDebug.class); private GlDebug() {} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/GlProgram.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/GlProgram.java new file mode 100644 index 000000000..d462eec7a --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/GlProgram.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.earlydisplay.render.backend.opengl; + +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import net.neoforged.fml.earlydisplay.render.backend.ELSRenderPipeline; +import net.neoforged.fml.earlydisplay.render.backend.VertexFormat; +import org.lwjgl.PointerBuffer; +import org.lwjgl.opengl.GL33C; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class GlProgram implements AutoCloseable { + private static final Logger LOGGER = LoggerFactory.getLogger(GlProgram.class); + + private final String name; + final int program; + private final Map uniformLocations; + private final Set warnedAboutUniforms = new HashSet<>(); + + private GlProgram(String name, int program, Map uniformLocations) { + this.name = name; + this.program = program; + this.uniformLocations = uniformLocations; + } + + public void setSampler(String name, int value) { + Integer location = this.uniformLocations.get(name); + if (location != null) { + GL33C.glUniform1i(location, value); + } else { + warnAboutMissingUniform(name); + } + } + + public void setUniform(String name, GlBuffer ubo) { + Integer location = this.uniformLocations.get(name); + if (location != null) { + GL33C.glBindBufferRange(GL33C.GL_UNIFORM_BUFFER, location, ubo.bufferId, 0, ubo.size()); + } else { + warnAboutMissingUniform(name); + } + } + + private void warnAboutMissingUniform(String name) { + if (this.warnedAboutUniforms.add(name)) { + LOGGER.error("Missing uniform '{}' in shader '{}'", name, this); + } + } + + @Override + public void close() { + GL33C.glDeleteProgram(this.program); + } + + @Override + public String toString() { + return "GlProgram{" + this.name + "@" + this.program + "}"; + } + + static GlProgram create(String name, ELSRenderPipeline pipeline, ByteBuffer vertexShaderSource, ByteBuffer fragmentShaderSource) { + int vertexShader = GL33C.glCreateShader(GL33C.GL_VERTEX_SHADER); + GlDebug.labelShader(vertexShader, "FML " + name + ".vert"); + int fragmentShader = GL33C.glCreateShader(GL33C.GL_FRAGMENT_SHADER); + GlDebug.labelShader(fragmentShader, "FML " + name + ".frag"); + + // Bind the source of our shaders to the ones created above + PointerBuffer sourcePointers = PointerBuffer.allocateDirect(1); + sourcePointers.put(0, fragmentShaderSource); + GL33C.glShaderSource(fragmentShader, sourcePointers, new int[] { fragmentShaderSource.remaining() }); + sourcePointers.put(0, vertexShaderSource); + GL33C.glShaderSource(vertexShader, sourcePointers, new int[] { vertexShaderSource.remaining() }); + + // Compile the vertex and fragment elementShader so that we can use them + GL33C.glCompileShader(vertexShader); + if (GL33C.glGetShaderi(vertexShader, GL33C.GL_COMPILE_STATUS) == GL33C.GL_FALSE) { + throw new IllegalStateException("VertexShader linkage failure. \n" + GL33C.glGetShaderInfoLog(vertexShader)); + } + GL33C.glCompileShader(fragmentShader); + if (GL33C.glGetShaderi(fragmentShader, GL33C.GL_COMPILE_STATUS) == GL33C.GL_FALSE) { + throw new IllegalStateException("FragmentShader linkage failure. \n" + GL33C.glGetShaderInfoLog(fragmentShader)); + } + + int program = GL33C.glCreateProgram(); + GlDebug.labelProgram(program, "EarlyDisplay program"); + for (VertexFormat.Element element : VertexFormat.Element.values()) { + int idx = pipeline.vertexFormat().findElement(element); + if (idx >= 0) { + GL33C.glBindAttribLocation(program, idx, element.name); + } + } + GL33C.glAttachShader(program, vertexShader); + GL33C.glAttachShader(program, fragmentShader); + GL33C.glLinkProgram(program); + if (GL33C.glGetProgrami(program, GL33C.GL_LINK_STATUS) == GL33C.GL_FALSE) { + throw new RuntimeException("ShaderProgram linkage failure. \n" + GL33C.glGetProgramInfoLog(program)); + } + + GL33C.glDetachShader(program, vertexShader); + GL33C.glDetachShader(program, fragmentShader); + GL33C.glDeleteShader(vertexShader); + GL33C.glDeleteShader(fragmentShader); + + int uniformCount = GL33C.glGetProgrami(program, GL33C.GL_ACTIVE_UNIFORMS); + Map uniformLocations = new HashMap<>(uniformCount); + if (pipeline.sampler() != null) { + int samplerLoc = GL33C.glGetUniformLocation(program, pipeline.sampler()); + if (samplerLoc != -1) { + uniformLocations.put(pipeline.sampler(), samplerLoc); + } + } + int uboBinding = 0; + for (String uniform : pipeline.uniforms()) { + int uboIndex = GL33C.glGetUniformBlockIndex(program, uniform); + if (uboIndex != -1) { + GL33C.glUniformBlockBinding(program, uboIndex, uboBinding); + uniformLocations.put(uniform, uboBinding); + uboBinding++; + } + } + + return new GlProgram(name, program, uniformLocations); + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/GlRenderBackend.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/GlRenderBackend.java new file mode 100644 index 000000000..b1f5905d6 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/GlRenderBackend.java @@ -0,0 +1,297 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.earlydisplay.render.backend.opengl; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Collection; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Set; +import net.neoforged.fml.earlydisplay.render.ElementShader; +import net.neoforged.fml.earlydisplay.render.backend.ELSBuffer; +import net.neoforged.fml.earlydisplay.render.backend.ELSBufferSlice; +import net.neoforged.fml.earlydisplay.render.backend.ELSRenderBackend; +import net.neoforged.fml.earlydisplay.render.backend.ELSRenderPipeline; +import net.neoforged.fml.earlydisplay.render.backend.ELSTexture; +import net.neoforged.fml.earlydisplay.render.backend.TextureFormat; +import net.neoforged.fml.earlydisplay.theme.ThemeColor; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnknownNullability; +import org.lwjgl.glfw.GLFW; +import org.lwjgl.opengl.GL; +import org.lwjgl.opengl.GL33C; +import org.lwjgl.opengl.GLCapabilities; +import org.lwjgl.system.MemoryUtil; + +final class GlRenderBackend extends ELSRenderBackend { + private final long windowHandle; + private final int maxTextureSize; + private final Map pipelines = new IdentityHashMap<>(); + private final QuadAutoIndexBuffer quadAutoIndexBuffer = new QuadAutoIndexBuffer(); + final VaoCache vaoCache = new VaoCache(); + + GlRenderBackend(long windowHandle) { + this.windowHandle = windowHandle; + this.maxTextureSize = GL33C.glGetInteger(GL33C.GL_MAX_TEXTURE_SIZE); + } + + @Override + public void preloadPipelines(@UnknownNullability Collection pipelines) { + for (ELSRenderPipeline pipeline : pipelines) { + ElementShader shader = pipeline.shader(); + try (var vertexShader = shader.loadVertexShader(); var fragmentShader = shader.loadFragmentShader()) { + GlProgram program = GlProgram.create(shader.getName(), pipeline, vertexShader.buffer(), fragmentShader.buffer()); + this.pipelines.put(pipeline, new GlCompiledPipeline(pipeline, program)); + } catch (IOException e) { + throw new RuntimeException("Failed to read shaders for " + shader.getName(), e); + } + } + } + + @Override + public GlTexture createTexture(String debugName, int width, int height, TextureFormat format, boolean linearFilter) { + int texId = GL33C.glGenTextures(); + GlState.bindTexture2D(texId); + GlDebug.labelTexture(texId, debugName); + GL33C.glTexParameteri(GL33C.GL_TEXTURE_2D, GL33C.GL_TEXTURE_WRAP_S, GL33C.GL_CLAMP_TO_EDGE); + GL33C.glTexParameteri(GL33C.GL_TEXTURE_2D, GL33C.GL_TEXTURE_WRAP_T, GL33C.GL_CLAMP_TO_EDGE); + GL33C.glTexParameteri(GL33C.GL_TEXTURE_2D, GL33C.GL_TEXTURE_MAG_FILTER, linearFilter ? GL33C.GL_LINEAR : GL33C.GL_NEAREST); + GL33C.glTexParameteri(GL33C.GL_TEXTURE_2D, GL33C.GL_TEXTURE_MIN_FILTER, linearFilter ? GL33C.GL_LINEAR : GL33C.GL_NEAREST); + GL33C.glTexImage2D(GL33C.GL_TEXTURE_2D, 0, GlConst.toGlInternalId(format), width, height, 0, GlConst.toGlExternalId(format), GL33C.GL_UNSIGNED_BYTE, (ByteBuffer) null); + return new GlTexture(debugName, width, height, format, texId); + } + + @Override + public void writeToTexture(ELSTexture texture, ByteBuffer pixels) { + TextureFormat format = texture.format(); + GlState.bindTexture2D(((GlTexture) texture).textureId); + GL33C.glPixelStorei(GL33C.GL_UNPACK_ROW_LENGTH, texture.width()); + GL33C.glPixelStorei(GL33C.GL_UNPACK_SKIP_PIXELS, 0); + GL33C.glPixelStorei(GL33C.GL_UNPACK_SKIP_ROWS, 0); + GL33C.glPixelStorei(GL33C.GL_UNPACK_ALIGNMENT, format.getComponents()); + GL33C.glTexSubImage2D(GL33C.GL_TEXTURE_2D, 0, 0, 0, texture.width(), texture.height(), GlConst.toGlExternalId(format), GL33C.GL_UNSIGNED_BYTE, pixels); + } + + @Override + public GlBuffer createBuffer(String label, Set usage, long size) { + int bufferId = GL33C.glGenBuffers(); + int bindTarget = GlConst.getBufferBindTarget(usage); + GL33C.glBindBuffer(bindTarget, bufferId); + GlDebug.labelBuffer(bufferId, label); + GL33C.glBufferData(bindTarget, size, GlConst.bufferUsageToGlEnum(usage)); + GL33C.glBindBuffer(bindTarget, 0); + return new GlBuffer(bufferId, usage, size); + } + + @Override + public GlBuffer createBuffer(String label, Set usage, ByteBuffer data) { + int bufferId = GL33C.glGenBuffers(); + int size = data.remaining(); + int bindTarget = GlConst.getBufferBindTarget(usage); + GL33C.glBindBuffer(bindTarget, bufferId); + GlDebug.labelBuffer(bufferId, label); + GL33C.glBufferData(bindTarget, data, GlConst.bufferUsageToGlEnum(usage)); + GL33C.glBindBuffer(bindTarget, 0); + return new GlBuffer(bufferId, usage, size); + } + + @Override + public void writeToBuffer(ELSBufferSlice destination, ByteBuffer data) { + GlBuffer buffer = (GlBuffer) destination.buffer(); + int bindTarget = GlConst.getBufferBindTarget(buffer.usage()); + GL33C.glBindBuffer(bindTarget, buffer.bufferId); + GL33C.glBufferSubData(bindTarget, destination.offset(), data); + GL33C.glBindBuffer(bindTarget, 0); + } + + @Override + public void copyBufferToBuffer(@UnknownNullability ELSBufferSlice source, ELSBufferSlice target) { + GL33C.glBindBuffer(GL33C.GL_COPY_READ_BUFFER, ((GlBufferSlice) source).buffer().bufferId); + GL33C.glBindBuffer(GL33C.GL_COPY_WRITE_BUFFER, ((GlBufferSlice) target).buffer().bufferId); + GL33C.glCopyBufferSubData(GL33C.GL_COPY_READ_BUFFER, GL33C.GL_COPY_WRITE_BUFFER, 0, 0, source.buffer().size()); + GL33C.glBindBuffer(GL33C.GL_COPY_READ_BUFFER, 0); + GL33C.glBindBuffer(GL33C.GL_COPY_WRITE_BUFFER, 0); + } + + @Override + public GlBuffer getQuadAutoIndexBuffer(int indexCount) { + return this.quadAutoIndexBuffer.acquire(indexCount); + } + + @Override + public GlRenderPass createRenderPass(String label, ELSTexture target, ThemeColor clearColor) { + GlDebug.pushGroup(label); + GlState.bindFramebuffer(((GlTexture) target).fbo()); + GlState.clearColor(clearColor.r(), clearColor.b(), clearColor.g(), clearColor.a()); + GL33C.glColorMask(true, true, true, true); + GL33C.glClear(GL33C.GL_COLOR_BUFFER_BIT | GL33C.GL_DEPTH_BUFFER_BIT); + GlState.viewport(0, 0, target.width(), target.height()); + return new GlRenderPass(this); + } + + @Override + public boolean startFrame(FramebufferSizeListener listener) { + int[] fbWidth = new int[1]; + int[] fbHeight = new int[1]; + GLFW.glfwGetFramebufferSize(windowHandle, fbWidth, fbHeight); + listener.accept(fbWidth[0], fbHeight[0]); + return true; + } + + @Override + public void presentTexture(ELSTexture texture, ThemeColor backgroundColor, int windowFBWidth, int windowFBHeight) { + int width = texture.width(); + int height = texture.height(); + GlState.viewport(0, 0, width, height); + + float wscale = ((float) windowFBWidth / width); + float hscale = ((float) windowFBHeight / height); + float scale = Math.min(wscale, hscale) / 2f; + int wleft = (int) (windowFBWidth * 0.5f - scale * width); + int wtop = (int) (windowFBHeight * 0.5f - scale * height); + int wright = (int) (windowFBWidth * 0.5f + scale * width); + int wbottom = (int) (windowFBHeight * 0.5f + scale * height); + + GlState.bindDrawFramebuffer(0); + GlState.bindReadFramebuffer(((GlTexture) texture).fbo()); + GlState.clearColor(backgroundColor.r(), backgroundColor.g(), backgroundColor.b(), 1f); + GL33C.glClear(GL33C.GL_COLOR_BUFFER_BIT | GL33C.GL_DEPTH_BUFFER_BIT); + GL33C.glBlitFramebuffer( + 0, + 0, + width, + height, + Math.clamp(wleft, 0, windowFBWidth), + Math.clamp(wtop, 0, windowFBHeight), + Math.clamp(wright, 0, windowFBWidth), + Math.clamp(wbottom, 0, windowFBHeight), + GL33C.GL_COLOR_BUFFER_BIT, + GL33C.GL_NEAREST); + GlState.bindFramebuffer(0); + + GLFW.glfwSwapBuffers(this.windowHandle); + } + + GlCompiledPipeline getCompiledPipeline(ELSRenderPipeline pipeline) { + GlCompiledPipeline compiledPipeline = this.pipelines.get(pipeline); + if (compiledPipeline == null) { + throw new IllegalArgumentException("Unrecognized pipeline: " + pipeline); + } + return compiledPipeline; + } + + @Override + public int getMaxTextureSize() { + return this.maxTextureSize; + } + + @Override + public long getWindowHandle() { + return this.windowHandle; + } + + @Override + public void acquireContextOwnership(boolean createContext) { + GLFW.glfwMakeContextCurrent(this.windowHandle); + if (createContext) { + GL.createCapabilities(); + } + } + + @Override + public void releaseContextOwnership() { + GLFW.glfwMakeContextCurrent(0L); + } + + @Override + public void guardResourceCleanup(Runnable cleanupTask) { + long previousContext = GLFW.glfwGetCurrentContext(); + GLCapabilities previousCaps; + try { + previousCaps = GL.getCapabilities(); + } catch (Throwable t) { + previousCaps = null; + } + + boolean needsToRestoreContext = previousContext != this.windowHandle; + if (needsToRestoreContext) { + GLFW.glfwMakeContextCurrent(this.windowHandle); + GL.createCapabilities(); + } + + try { + cleanupTask.run(); + + // Clear out bound resources as the cleanup task most likely destroyed them + GlState.bindElementArrayBuffer(0); + GlState.bindFramebuffer(0); + GlState.bindTexture2D(0); + GlState.bindVertexArray(0); + } finally { + if (needsToRestoreContext) { + GLFW.glfwMakeContextCurrent(previousContext); + GL.setCapabilities(previousCaps); + } + } + } + + @Override + public void close() { + this.quadAutoIndexBuffer.close(); + } + + @Override + public String name() { + return "OpenGL"; + } + + private final class QuadAutoIndexBuffer implements AutoCloseable { + private static final Set BUFFER_USAGE = Set.of(ELSBuffer.Usage.INDEX); + private static final int QUAD_STRIDE = 4; + private static final int INDEX_STRIDE = 6; + + @Nullable + private GlBuffer buffer; + private int indexCount; + + GlBuffer acquire(int indexCount) { + if (this.buffer == null || this.indexCount < indexCount) { + int bufferSize = indexCount * 4; + ByteBuffer data = MemoryUtil.memAlloc(bufferSize); + try { + for (int i = 0; i < indexCount; i += INDEX_STRIDE) { + int idx = i * QUAD_STRIDE / INDEX_STRIDE; + data.putInt(idx); + data.putInt(idx + 1); + data.putInt(idx + 2); + data.putInt(idx + 2); + data.putInt(idx + 3); + data.putInt(idx); + } + data.flip(); + if (this.buffer != null) { + this.buffer.close(); + } + this.buffer = GlRenderBackend.this.createBuffer("ELS quad auto index buffer", BUFFER_USAGE, data); + GlState.bindElementArrayBuffer(0); + } finally { + MemoryUtil.memFree(data); + } + this.indexCount = indexCount; + } + return this.buffer; + } + + @Override + public void close() { + if (this.buffer != null) { + this.buffer.close(); + this.buffer = null; + } + } + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/GlRenderPass.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/GlRenderPass.java new file mode 100644 index 000000000..d136676f2 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/GlRenderPass.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.earlydisplay.render.backend.opengl; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import net.neoforged.fml.earlydisplay.render.backend.ELSBuffer; +import net.neoforged.fml.earlydisplay.render.backend.ELSBufferSlice; +import net.neoforged.fml.earlydisplay.render.backend.ELSRenderPass; +import net.neoforged.fml.earlydisplay.render.backend.ELSRenderPipeline; +import net.neoforged.fml.earlydisplay.render.backend.ELSTexture; +import net.neoforged.fml.earlydisplay.render.backend.VertexFormat; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.opengl.GL33C; + +final class GlRenderPass implements ELSRenderPass { + private final GlRenderBackend backend; + private final Map textures = new HashMap<>(); + private final Map uniforms = new HashMap<>(); + @Nullable + private GlCompiledPipeline pipeline; + @Nullable + private GlBufferSlice vertexBuffer; + @Nullable + private GlBuffer indexBuffer; + + GlRenderPass(GlRenderBackend backend) { + this.backend = backend; + } + + @Override + public void setViewport(int x, int y, int width, int height) { + GlState.viewport(x, y, width, height); + } + + @Override + public void enableScissor(int x, int y, int width, int height) { + GlState.scissorTest(true); + GlState.scissorBox(x, y, width, height); + } + + @Override + public void disableScissor() { + GlState.scissorTest(false); + } + + @Override + public void bindPipeline(ELSRenderPipeline pipeline) { + this.pipeline = this.backend.getCompiledPipeline(pipeline); + } + + @Override + public void bindTexture(String name, @Nullable ELSTexture texture) { + if (texture != null) { + this.textures.put(name, (GlTexture) texture); + } else { + this.textures.remove(name); + } + } + + @Override + public void bindUniform(String name, ELSBuffer buffer) { + this.uniforms.put(name, (GlBuffer) buffer); + } + + @Override + public void bindVertexBuffer(ELSBufferSlice buffer) { + this.vertexBuffer = (GlBufferSlice) buffer; + } + + @Override + public void bindIndexBuffer(@Nullable ELSBuffer buffer) { + this.indexBuffer = (GlBuffer) buffer; + } + + @Override + public void draw(int vertexCount) { + setupPipelineState(false); + GL33C.glDrawArrays(GL33C.GL_TRIANGLES, 0, vertexCount); + } + + @Override + public void drawIndexed(int indexCount) { + setupPipelineState(true); + GL33C.glDrawElements(GL33C.GL_TRIANGLES, indexCount, GL33C.GL_UNSIGNED_INT, 0); + } + + private void setupPipelineState(boolean indexed) { + Objects.requireNonNull(this.pipeline, "No pipeline set"); + Objects.requireNonNull(this.vertexBuffer, "No vertex buffer set"); + if (indexed) { + Objects.requireNonNull(this.indexBuffer, "No index buffer set"); + } + + GlProgram program = this.pipeline.program(); + GlState.useProgram(program.program); + + String sampler = this.pipeline.info().sampler(); + if (sampler != null) { + program.setSampler(sampler, 0); + GlTexture texture = this.textures.get(sampler); + GlState.bindTexture2D(texture != null ? texture.textureId : 0); + GlState.bindSampler(0); + } else { + GlState.bindTexture2D(0); + } + + for (String uniform : this.pipeline.info().uniforms()) { + GlBuffer ubo = this.uniforms.get(uniform); + if (ubo != null) { + program.setUniform(uniform, ubo); + } + } + + this.backend.vaoCache.bindVertexBuffer(VertexFormat.POS_TEX_COLOR, this.vertexBuffer); + GlState.bindElementArrayBuffer(indexed ? indexBuffer.bufferId : 0); + + GlState.enableBlend(true); + GlState.blendFuncSeparate(GL33C.GL_SRC_ALPHA, GL33C.GL_ONE_MINUS_SRC_ALPHA, GL33C.GL_ZERO, GL33C.GL_ONE); + } + + @Override + public void close() { + GlState.bindFramebuffer(0); + GlDebug.popGroup(); + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/GlRenderer.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/GlRenderer.java new file mode 100644 index 000000000..d128e00bb --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/GlRenderer.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.earlydisplay.render.backend.opengl; + +import net.neoforged.fml.earlydisplay.render.backend.ELSRenderBackend; +import net.neoforged.fml.loading.FMLConfig; +import org.lwjgl.glfw.GLFW; +import org.lwjgl.opengl.GL; +import org.lwjgl.opengl.GL11C; +import org.lwjgl.opengl.GL33C; +import org.lwjgl.opengl.GLCapabilities; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class GlRenderer { + private static final Logger LOGGER = LoggerFactory.getLogger(GlRenderer.class); + + public static void configureWindowHints() { + GLFW.glfwWindowHint(GLFW.GLFW_CLIENT_API, GLFW.GLFW_OPENGL_API); + GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_CREATION_API, GLFW.GLFW_NATIVE_CONTEXT_API); + GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, 3); + GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MINOR, 3); + GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_PROFILE, GLFW.GLFW_OPENGL_CORE_PROFILE); + GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_FORWARD_COMPAT, GL11C.GL_TRUE); + + if (FMLConfig.getBoolConfigValue(FMLConfig.ConfigValue.DEBUG_OPENGL)) { + LOGGER.info("Requesting the creation of an OpenGL debug context"); + GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_DEBUG_CONTEXT, GL11C.GL_TRUE); + } + } + + public static ELSRenderBackend setupBackend(long windowHandle) { + GLFW.glfwMakeContextCurrent(windowHandle); + GLFW.glfwSwapInterval(1); + GLCapabilities capabilities = GL.createCapabilities(); + GlDebug.setCapabilities(capabilities); + LOGGER.info("GL info: {} GL version {}, {}", GL33C.glGetString(GL33C.GL_RENDERER), GL33C.glGetString(GL33C.GL_VERSION), GL33C.glGetString(GL33C.GL_VENDOR)); + GlState.readFromOpenGL(); + return new GlRenderBackend(windowHandle); + } + + private GlRenderer() {} +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/GlState.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/GlState.java similarity index 79% rename from earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/GlState.java rename to earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/GlState.java index f0a32c147..2c7ceb65f 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/GlState.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/GlState.java @@ -3,12 +3,11 @@ * SPDX-License-Identifier: LGPL-2.1-only */ -package net.neoforged.fml.earlydisplay.render; +package net.neoforged.fml.earlydisplay.render.backend.opengl; import static org.lwjgl.opengl.GL11C.GL_SCISSOR_BOX; import static org.lwjgl.opengl.GL11C.GL_SCISSOR_TEST; import static org.lwjgl.opengl.GL11C.glScissor; -import static org.lwjgl.opengl.GL20C.glIsProgram; import static org.lwjgl.opengl.GL32C.GL_ACTIVE_TEXTURE; import static org.lwjgl.opengl.GL32C.GL_ARRAY_BUFFER; import static org.lwjgl.opengl.GL32C.GL_ARRAY_BUFFER_BINDING; @@ -55,7 +54,7 @@ * This class tracks the current state of various OpenGL state elements and only applies changes * when necessary, reducing overhead from redundant state changes. */ -public final class GlState { +final class GlState { // Viewport state private static int viewportX; private static int viewportY; @@ -377,71 +376,4 @@ public static void scissorTest(boolean enabled) { scissorEnabled = enabled; } } - - /** - * A snapshot of the OpenGL state. - */ - public record StateSnapshot( - int viewportX, int viewportY, int viewportWidth, int viewportHeight, - float clearColorRed, float clearColorGreen, float clearColorBlue, float clearColorAlpha, - boolean blendEnabled, - int blendSrcRGB, int blendDstRGB, int blendSrcAlpha, int blendDstAlpha, - int currentProgram, - int boundTexture2D, int boundSampler, int activeTextureUnit, - int boundVertexArray, - int boundDrawFramebuffer, int boundReadFramebuffer, - int boundElementArrayBuffer, int boundArrayBuffer, - boolean scissorEnabled, int[] scissorBox) {} - - /** - * Creates a snapshot of the current OpenGL state. - * - * @return A StateSnapshot object containing the current state - */ - public static StateSnapshot createSnapshot() { - return new StateSnapshot( - viewportX, viewportY, viewportWidth, viewportHeight, - clearColorRed, clearColorGreen, clearColorBlue, clearColorAlpha, - blendEnabled, - blendSrcRGB, blendDstRGB, blendSrcAlpha, blendDstAlpha, - currentProgram, - boundTexture2D, boundSampler, activeTextureUnit, - boundVertexArray, - boundDrawFramebuffer, boundReadFramebuffer, - boundElementArrayBuffer, boundArrayBuffer, - scissorEnabled, scissorBox); - } - - /** - * Applies the state from a snapshot to both this state manager and OpenGL. - * - * @param snapshot The snapshot to apply - */ - public static void applySnapshot(StateSnapshot snapshot) { - viewport(snapshot.viewportX, snapshot.viewportY, snapshot.viewportWidth, snapshot.viewportHeight); - clearColor(snapshot.clearColorRed, snapshot.clearColorGreen, snapshot.clearColorBlue, snapshot.clearColorAlpha); - enableBlend(snapshot.blendEnabled); - blendFuncSeparate(snapshot.blendSrcRGB, snapshot.blendDstRGB, snapshot.blendSrcAlpha, snapshot.blendDstAlpha); - // The program might have been flagged for deletion and may no longer be available - if (glIsProgram(snapshot.currentProgram)) { - useProgram(snapshot.currentProgram); - } else { - useProgram(0); - } - bindTexture2D(snapshot.boundTexture2D); - bindSampler(snapshot.boundSampler); - activeTexture(snapshot.activeTextureUnit); - bindVertexArray(snapshot.boundVertexArray); - // Handle framebuffers - check if both are the same - if (snapshot.boundDrawFramebuffer == snapshot.boundReadFramebuffer) { - bindFramebuffer(snapshot.boundDrawFramebuffer); - } else { - bindDrawFramebuffer(snapshot.boundDrawFramebuffer); - bindReadFramebuffer(snapshot.boundReadFramebuffer); - } - bindElementArrayBuffer(snapshot.boundElementArrayBuffer); - bindArrayBuffer(snapshot.boundArrayBuffer); - scissorTest(snapshot.scissorEnabled); - scissorBox(snapshot.scissorBox[0], snapshot.scissorBox[1], snapshot.scissorBox[2], snapshot.scissorBox[3]); - } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/GlTexture.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/GlTexture.java new file mode 100644 index 000000000..06b9867e3 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/GlTexture.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.earlydisplay.render.backend.opengl; + +import net.neoforged.fml.earlydisplay.render.backend.ELSTexture; +import net.neoforged.fml.earlydisplay.render.backend.TextureFormat; +import org.lwjgl.opengl.GL33C; + +final class GlTexture implements ELSTexture { + private final String name; + private final int width; + private final int height; + private final TextureFormat format; + final int textureId; + private int fboId = -1; + private boolean closed; + + GlTexture(String name, int width, int height, TextureFormat format, int textureId) { + this.name = name; + this.width = width; + this.height = height; + this.format = format; + this.textureId = textureId; + } + + @Override + public int width() { + return this.width; + } + + @Override + public int height() { + return this.height; + } + + @Override + public TextureFormat format() { + return this.format; + } + + public int fbo() { + if (this.fboId == -1) { + this.fboId = GL33C.glGenFramebuffers(); + GlState.bindFramebuffer(this.fboId); + GL33C.glFramebufferTexture2D(GL33C.GL_FRAMEBUFFER, GL33C.GL_COLOR_ATTACHMENT0, GL33C.GL_TEXTURE_2D, this.textureId, 0); + GlState.bindFramebuffer(0); + GlDebug.labelFramebuffer(this.fboId, this.name); + } + return this.fboId; + } + + @Override + public void close() { + if (closed) { + return; + } + + closed = true; + if (this.fboId != -1) { + GL33C.glDeleteFramebuffers(this.fboId); + } + GL33C.glDeleteTextures(this.textureId); + } + + @Override + public String toString() { + return "GlTexture{" + this.name + "}"; + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/VaoCache.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/VaoCache.java new file mode 100644 index 000000000..da4435ead --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/opengl/VaoCache.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.earlydisplay.render.backend.opengl; + +import java.util.EnumMap; +import java.util.Map; +import net.neoforged.fml.earlydisplay.render.backend.VertexFormat; +import org.lwjgl.opengl.GL33C; + +final class VaoCache implements AutoCloseable { + private final Map cache = new EnumMap<>(VertexFormat.class); + + VAO bindVertexBuffer(VertexFormat format, GlBufferSlice vertexBuffer) { + VAO vao = this.cache.get(format); + boolean enable = false; + if (vao == null) { + int id = GL33C.glGenVertexArrays(); + GlState.bindVertexArray(id); + GlDebug.labelVertexArray(id, format.name()); + vao = new VAO(id); + this.cache.put(format, vao); + enable = true; + } else { + GlState.bindVertexArray(vao.id); + } + + GL33C.glBindBuffer(GL33C.GL_ARRAY_BUFFER, vertexBuffer.buffer().bufferId); + int stride = format.stride; + long offset = vertexBuffer.offset(); + for (int i = 0; i < format.elementCount(); i++) { + VertexFormat.Element element = format.element(i); + if (enable) { + GL33C.glEnableVertexAttribArray(i); + } + switch (element) { + case POS, TEX -> GL33C.glVertexAttribPointer(i, element.count, GL33C.GL_FLOAT, false, stride, offset); + case COLOR -> GL33C.glVertexAttribPointer(i, element.count, GL33C.GL_UNSIGNED_BYTE, true, stride, offset); + } + offset += element.width; + } + + return vao; + } + + @Override + public void close() { + cache.values().forEach(VAO::close); + } + + record VAO(int id) implements AutoCloseable { + @Override + public void close() { + GL33C.glDeleteVertexArrays(id); + } + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/package-info.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/package-info.java new file mode 100644 index 000000000..c6fb62a5a --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/backend/package-info.java @@ -0,0 +1,9 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +@ApiStatus.Internal +package net.neoforged.fml.earlydisplay.render.backend; + +import org.jetbrains.annotations.ApiStatus; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ImageElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ImageElement.java index f87c03de6..4a858e180 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ImageElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ImageElement.java @@ -8,14 +8,15 @@ import net.neoforged.fml.earlydisplay.render.MaterializedTheme; import net.neoforged.fml.earlydisplay.render.RenderContext; import net.neoforged.fml.earlydisplay.render.Texture; +import net.neoforged.fml.earlydisplay.render.backend.ELSRenderBackend; import net.neoforged.fml.earlydisplay.theme.elements.ThemeImageElement; public class ImageElement extends RenderElement { private final Texture texture; - public ImageElement(ThemeImageElement element, MaterializedTheme theme) { + public ImageElement(ELSRenderBackend backend, ThemeImageElement element, MaterializedTheme theme) { super(element, theme); - this.texture = Texture.create(element.texture(), theme.externalThemeDirectory()); + this.texture = Texture.create(backend, element.texture(), theme.externalThemeDirectory()); } @Override diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/MojangLogoElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/MojangLogoElement.java index 27038faea..a2fc91eed 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/MojangLogoElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/MojangLogoElement.java @@ -9,6 +9,7 @@ import net.neoforged.fml.earlydisplay.render.MaterializedTheme; import net.neoforged.fml.earlydisplay.render.RenderContext; import net.neoforged.fml.earlydisplay.render.Texture; +import net.neoforged.fml.earlydisplay.render.backend.ELSRenderBackend; import net.neoforged.fml.earlydisplay.theme.ImageLoader; import net.neoforged.fml.earlydisplay.theme.NativeBuffer; import net.neoforged.fml.earlydisplay.theme.TextureScaling; @@ -29,7 +30,7 @@ public class MojangLogoElement extends RenderElement { private final Texture mojangLogo; - public MojangLogoElement(ThemeMojangLogoElement element, MaterializedTheme theme) { + public MojangLogoElement(ELSRenderBackend backend, ThemeMojangLogoElement element, MaterializedTheme theme) { super(element, theme); // Try the context classloader first, but if we cannot find a logo there, @@ -48,7 +49,7 @@ public MojangLogoElement(ThemeMojangLogoElement element, MaterializedTheme theme if (loadResult instanceof ImageLoader.Result.Error(Exception exception)) { LOGGER.debug("Failed to load Mojang logo from {}: {}", logoPath, exception); } else if (loadResult instanceof ImageLoader.Result.Success(UncompressedImage image)) { - mojangLogo = Texture.create(image, "mojang logo", new TextureScaling.Stretch(512, 128, true), null); + mojangLogo = Texture.create(backend, image, "mojang logo", new TextureScaling.Stretch(512, 128, true), null); break; } } catch (IOException e) { diff --git a/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/gui.vert b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/gui.vert index ca5f0cf6b..5a60465a0 100644 --- a/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/gui.vert +++ b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/gui.vert @@ -1,6 +1,9 @@ #version 150 core -uniform vec2 screenSize; +layout(std140) uniform screenSize { + vec2 screenSizeVec; +}; + in vec2 position; in vec2 uv; in vec4 color; @@ -10,5 +13,5 @@ out vec4 fColour; void main() { fTex = uv; fColour = color; - gl_Position = vec4((position / screenSize) * 2 - 1, 0.0, 1.0); + gl_Position = vec4((position / screenSizeVec) * 2 - 1, 0.0, 1.0) * vec4(1.0, -1.0, 1.0, 1.0); } diff --git a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/render/SimpleFontTest.java b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/render/SimpleFontTest.java index 2bf5d4de0..a2c64a84b 100644 --- a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/render/SimpleFontTest.java +++ b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/render/SimpleFontTest.java @@ -8,20 +8,26 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.IOException; +import net.neoforged.fml.earlydisplay.render.backend.ELSRenderBackend; +import net.neoforged.fml.earlydisplay.render.backend.opengl.GlRenderer; import net.neoforged.fml.earlydisplay.theme.ThemeResource; import org.junit.jupiter.api.AutoClose; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.lwjgl.glfw.GLFW; @ExtendWith(WithOffScreenGLSurface.class) class SimpleFontTest { + @AutoClose + private ELSRenderBackend backend; @AutoClose private SimpleFont font; @BeforeEach void setUp() throws IOException { - font = new SimpleFont(new ThemeResource("Monocraft.ttf"), null); + backend = GlRenderer.setupBackend(GLFW.glfwGetCurrentContext()); + font = new SimpleFont(backend, new ThemeResource("Monocraft.ttf"), null); } @Test diff --git a/loader/src/main/java/net/neoforged/fml/loading/EarlyLoadingScreenController.java b/loader/src/main/java/net/neoforged/fml/loading/EarlyLoadingScreenController.java index 19bf0bb65..4ee03a419 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/EarlyLoadingScreenController.java +++ b/loader/src/main/java/net/neoforged/fml/loading/EarlyLoadingScreenController.java @@ -5,6 +5,7 @@ package net.neoforged.fml.loading; +import java.util.function.Supplier; import org.jetbrains.annotations.Nullable; /** @@ -24,13 +25,11 @@ static EarlyLoadingScreenController current() { *

* This method can only be called once and once this method is called, any off-thread * interaction with the window seizes. - * - * @return The GLFW window handle for the window in a state that can be used by the game. */ - long takeOverGlfwWindow(); + void handOverToMinecraft(Supplier renderBackend); /** - * After calling {@linkplain #takeOverGlfwWindow() taking over} the main window, the game may still want to + * After calling {@linkplain #handOverToMinecraft(Supplier) taking over} the main window, the game may still want to * periodically ask the loading screen to update itself independently. It will call this method to do so. */ void periodicTick(); diff --git a/tests/src/moddevTest/java/TestEarlyDisplay.java b/tests/src/moddevTest/java/TestEarlyDisplay.java index a477deaf0..8198b4887 100644 --- a/tests/src/moddevTest/java/TestEarlyDisplay.java +++ b/tests/src/moddevTest/java/TestEarlyDisplay.java @@ -8,17 +8,19 @@ import java.nio.file.Paths; import java.util.concurrent.atomic.AtomicBoolean; import net.neoforged.fml.earlydisplay.DisplayWindow; +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.loading.FMLConfig; import net.neoforged.fml.loading.FMLPaths; import net.neoforged.fml.loading.ProgramArgs; import net.neoforged.fml.loading.progress.StartupNotificationManager; import org.lwjgl.glfw.GLFW; -import org.lwjgl.opengl.GL; public class TestEarlyDisplay { public static void main(String[] args) throws Exception { System.setProperty("java.awt.headless", "true"); - //System.setProperty("fml.earlyWindowDarkMode", "true"); + System.setProperty("fml.earlyWindowDarkMode", "true"); FMLPaths.loadAbsolutePaths(findProjectRoot()); FMLConfig.load(); @@ -33,18 +35,21 @@ public static void main(String[] args) throws Exception { AtomicBoolean closed = new AtomicBoolean(false); // Render once, then take over the window to test that it still works - periodicTick.run(); - long windowId = window.takeOverGlfwWindow(); - - // The context moves to the main thread now - GL.createCapabilities(); + while (!LoadingScreenRenderer.rendered) { + periodicTick.run(); + } + long windowHandle = window.getWindowHandle(); + ELSRenderBackend[] backend = new ELSRenderBackend[1]; + window.handOverToMinecraft(() -> backend[0] = GlRenderer.setupBackend(windowHandle), false); + backend[0].acquireContextOwnership(true); - GLFW.glfwSetWindowCloseCallback(windowId, window1 -> { + GLFW.glfwSetWindowCloseCallback(windowHandle, _ -> { window.close(); closed.set(true); }); StartupNotificationManager.addProgressBar("Test Bar", 20).setAbsolute(10); + StartupNotificationManager.addProgressBar("More Test Bar", 0); while (!closed.get()) { try { diff --git a/tests/src/moddevTest/java/TestErrorDisplay.java b/tests/src/moddevTest/java/TestErrorDisplay.java index a47b85cbf..cb07fb1a1 100644 --- a/tests/src/moddevTest/java/TestErrorDisplay.java +++ b/tests/src/moddevTest/java/TestErrorDisplay.java @@ -8,12 +8,10 @@ import java.util.List; import net.neoforged.fml.ModLoadingIssue; import net.neoforged.fml.earlydisplay.DisplayWindow; -import net.neoforged.fml.earlydisplay.error.ErrorDisplay; import net.neoforged.fml.earlydisplay.render.LoadingScreenRenderer; import net.neoforged.fml.loading.FMLConfig; import net.neoforged.fml.loading.FMLPaths; import net.neoforged.fml.loading.ProgramArgs; -import org.lwjgl.opengl.GL; public class TestErrorDisplay { public static void main(String[] args) throws Exception { @@ -32,11 +30,9 @@ public static void main(String[] args) throws Exception { window.setNeoForgeVersion("21.5.123-beta"); // Render at least one frame of the loading screen, then take over the window to display the error window - while (!LoadingScreenRenderer.rendered) + while (!LoadingScreenRenderer.rendered) { periodicTick.run(); - long windowId = window.takeOverGlfwWindow(); - GL.createCapabilities(); - window.close(); + } List issues = new ArrayList<>(); String suffix = "\nThisIsAVeryLongLineOfTextWithNoSpacesButItShouldStillBeWrappedToFitTheErrorScreensListWidth"; @@ -59,6 +55,6 @@ public static void main(String[] args) throws Exception { issues.add(ModLoadingIssue.warning("fml.modloadingissue.discouragedmod", "dimodid", "ownermodid", "somerange", "1.2.3", "fml.modloadingissue.discouragedmod.noreason")); - ErrorDisplay.fatal(windowId, null, null, issues, Path.of("./tests/mods"), Path.of("./logs/latest.log"), null); + window.displayFatalErrorAndExit(issues, Path.of("./tests/mods"), Path.of("./logs/latest.log"), null); } }