From 1e6fa7971670ada8a7972274d31d6c2ad3edf7b5 Mon Sep 17 00:00:00 2001 From: JeremyStarTM Date: Tue, 15 Oct 2024 03:30:49 +0200 Subject: [PATCH] Implement experimental window icon support --- .../staropensource/engine/testapp/Main.java | 6 +- windowing/glfw/build.gradle | 11 +-- .../glfw/implementable/GlfwWindow.java | 76 ++++++++++++++++++- windowing/glfw/src/main/java/module-info.java | 2 +- .../windowing/implementable/Window.java | 66 +++++++++++++++- 5 files changed, 148 insertions(+), 13 deletions(-) diff --git a/testapp/src/main/java/de/staropensource/engine/testapp/Main.java b/testapp/src/main/java/de/staropensource/engine/testapp/Main.java index 55bea013..ecae78c9 100644 --- a/testapp/src/main/java/de/staropensource/engine/testapp/Main.java +++ b/testapp/src/main/java/de/staropensource/engine/testapp/Main.java @@ -134,9 +134,11 @@ public final class Main { // Create window Window window; try { + // Build window window = new Window.Builder() .setName("sosengine-testapp") .setTitle("test application window") + //.setIcons(new Path[]{ new File("/home/jeremystartm/Code/StarOpenSource/Engine/dist/branding/current.png").toPath() }) .setSize(new Vec2i(960, 540)) .setPosition(new Vec2i(10, 10)) .build(); @@ -145,6 +147,7 @@ public final class Main { return; } + // Render loop LinkedHashMap<@NotNull Window, @NotNull Throwable> renderLoopFailures = WindowingSubsystem .getInstance() .getApi() @@ -153,6 +156,8 @@ public final class Main { if (shutdown || window.isClosureRequested()) Engine.getInstance().shutdown(); }); + + // Print render loop failures StringBuilder message = new StringBuilder(); message.append("Render loop failed on some windows:\n"); @@ -172,7 +177,6 @@ public final class Main { System.err.println(Miscellaneous.getStackTraceHeader(exception)); System.err.println(Miscellaneous.getStackTraceAsString(exception, true)); - // Halt JVM Runtime.getRuntime().halt(255); } } diff --git a/windowing/glfw/build.gradle b/windowing/glfw/build.gradle index 77b39ce1..d12f687f 100644 --- a/windowing/glfw/build.gradle +++ b/windowing/glfw/build.gradle @@ -31,13 +31,12 @@ switch (OperatingSystem.current()) { case OperatingSystem.LINUX: project.dependencyLwjglNatives = "natives-linux" def osArch = System.getProperty("os.arch") - if (osArch.startsWith("arm") || osArch.startsWith("aarch64")) { + if (osArch.startsWith("arm") || osArch.startsWith("aarch64")) project.dependencyLwjglNatives += osArch.contains("64") || osArch.startsWith("armv8") ? "-arm64" : "-arm32" - } else if (osArch.startsWith("ppc")) { + else if (osArch.startsWith("ppc")) project.dependencyLwjglNatives += "-ppc64le" - } else if (osArch.startsWith("riscv")) { + else if (osArch.startsWith("riscv")) project.dependencyLwjglNatives += "-riscv64" - } break case OperatingSystem.MAC_OS: project.dependencyLwjglNatives = System.getProperty("os.arch").startsWith("aarch64") ? "natives-macos-arm64" : "natives-macos" @@ -64,8 +63,10 @@ dependencies { implementation(platform("org.lwjgl:lwjgl-bom:${dependencyLwjgl}")) implementation("org.lwjgl:lwjgl") implementation("org.lwjgl:lwjgl-glfw") + implementation("org.lwjgl:lwjgl-stb") runtimeOnly("org.lwjgl:lwjgl::${dependencyLwjglNatives}") runtimeOnly("org.lwjgl:lwjgl-glfw::${dependencyLwjglNatives}") + runtimeOnly("org.lwjgl:lwjgl-stb::${dependencyLwjglNatives}") if (project.dependencyLwjglNatives == "natives-macos" || project.dependencyLwjglNatives == "natives-macos-arm64") runtimeOnly("org.lwjgl:lwjgl-vulkan::${dependencyLwjglNatives}") // -> Testing <- @@ -126,7 +127,7 @@ test { useJUnitPlatform() // Pass test configuration to test VMs - Map testConfiguration = new HashMap<>(); + Map testConfiguration = new HashMap<>() for (String property : project.properties.keySet()) if (property.startsWith("test.")) testConfiguration.put(property, project.properties.get(property).toString()) diff --git a/windowing/glfw/src/main/java/de/staropensource/engine/windowing/glfw/implementable/GlfwWindow.java b/windowing/glfw/src/main/java/de/staropensource/engine/windowing/glfw/implementable/GlfwWindow.java index 3b2479eb..3933022c 100644 --- a/windowing/glfw/src/main/java/de/staropensource/engine/windowing/glfw/implementable/GlfwWindow.java +++ b/windowing/glfw/src/main/java/de/staropensource/engine/windowing/glfw/implementable/GlfwWindow.java @@ -19,6 +19,7 @@ package de.staropensource.engine.windowing.glfw.implementable; +import de.staropensource.engine.base.logging.Logger; import de.staropensource.engine.base.type.vector.Vec2i; import de.staropensource.engine.base.utility.Miscellaneous; import de.staropensource.engine.windowing.WindowingSubsystemConfiguration; @@ -33,13 +34,21 @@ import de.staropensource.engine.windowing.implementable.Window; import de.staropensource.engine.windowing.type.window.VsyncMode; import de.staropensource.engine.windowing.type.window.WindowMode; import lombok.Getter; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.glfw.GLFWImage; import org.lwjgl.glfw.GLFWKeyCallback; import org.lwjgl.glfw.GLFWMouseButtonCallback; +import org.lwjgl.stb.STBImage; import org.lwjgl.system.MemoryStack; import org.lwjgl.system.MemoryUtil; +import java.nio.ByteBuffer; import java.nio.IntBuffer; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; import static org.lwjgl.glfw.GLFW.*; @@ -84,6 +93,7 @@ public final class GlfwWindow extends Window { * * @param name name * @param title title + * @param icons icons * @param size size * @param minimumSize minimum size * @param maximumSize maximum size @@ -99,8 +109,8 @@ public final class GlfwWindow extends Window { * @throws Exception stuff thrown by the {@link #initializeWindow()} and {@link #render()} methods of the implementing windowing API * @since v1-alpha2 */ - public GlfwWindow(@NotNull String name, @NotNull String title, @NotNull Vec2i size, @NotNull Vec2i minimumSize, @NotNull Vec2i maximumSize, @NotNull Vec2i position, @NotNull WindowMode windowMode, @NotNull Monitor monitor, boolean resizable, boolean borderless, boolean focusable, boolean onTop, boolean transparent, boolean rendering) throws Exception { - super(name, title, size, minimumSize, maximumSize, position, windowMode, monitor, resizable, borderless, focusable, onTop, transparent, rendering); + public GlfwWindow(@NotNull String name, @NotNull String title, @NotNull Path @Nullable [] icons, @NotNull Vec2i size, @NotNull Vec2i minimumSize, @NotNull Vec2i maximumSize, @NotNull Vec2i position, @NotNull WindowMode windowMode, @NotNull Monitor monitor, boolean resizable, boolean borderless, boolean focusable, boolean onTop, boolean transparent, boolean rendering) throws Exception { + super(name, title, icons, size, minimumSize, maximumSize, position, windowMode, monitor, resizable, borderless, focusable, onTop, transparent, rendering); } /** {@inheritDoc} */ @@ -163,6 +173,7 @@ public final class GlfwWindow extends Window { glfwSetMouseButtonCallback(identifier, mouseButtonCallback); // Update the window state + setIcons(getIcons()); setSize(getSize()); setMinimumSize(getMinimumSize()); setMaximumSize(getMaximumSize()); @@ -342,6 +353,67 @@ public final class GlfwWindow extends Window { glfwSetWindowTitle(identifierLong, title); } + /** {@inheritDoc} */ + @ApiStatus.Experimental + @Override + public void setIcons(@NotNull Path @Nullable [] icons) { + getLogger().warn("GlfwWindow#setIcons is experimental and may cause engine or JVM crashes. Here be dragons!"); + + // Ensure the window is not terminated + if (isTerminated()) + return; + + super.setIcons(icons); + if (icons != null) + try (GLFWImage.Buffer iconsBuffer = GLFWImage.malloc(icons.length)) { + getLogger().diag("GlfwWindow#setIcons » icons.length = " + icons.length); + + List iconBuffers = new ArrayList<>(); + IntBuffer width = MemoryUtil.memAllocInt(1); + IntBuffer height = MemoryUtil.memAllocInt(1); + IntBuffer channels = MemoryUtil.memAllocInt(1); + + for (Path filepath : icons) { + getLogger().diag("GlfwWindow#setIcons » iterating icons » " + iconBuffers.size() + " » " + filepath); + // Load icon + getLogger().diag("GlfwWindow#setIcons » loading icon"); + iconBuffers.add(STBImage.stbi_load(filepath.toAbsolutePath().toString(), width, height, channels, 4)); + + if (iconBuffers.getLast() == null) { + getLogger().warn("Icon " + iconsBuffer.position() + " could not be loaded" + (STBImage.stbi_failure_reason() == null ? "" : ": " + STBImage.stbi_failure_reason())); + continue; + } + + // Save into 'iconsBuffer' + getLogger().diag("GlfwWindow#setIcons » saving into buffer"); + iconsBuffer + .position(iconsBuffer.position() + 1) + .width(width.get(0)) + .height(height.get(0)) + .pixels(iconBuffers.getLast()); + } + getLogger().diag("GlfwWindow#setIcons » out of iteration"); + + // Set icons + getLogger().diag("GlfwWindow#setIcons » setting position"); + iconsBuffer.position(0); + getLogger().diag("GlfwWindow#setIcons » setting icons"); + Logger.flushLogMessages(); + glfwSetWindowIcon(identifierLong, iconsBuffer); + + // Free icons + getLogger().diag("GlfwWindow#setIcons » freeing icons"); + for (ByteBuffer buffer : iconBuffers) + if (buffer != null) { + getLogger().diag("GlfwWindow#setIcons » freeing buffer"); + STBImage.stbi_image_free(buffer); + } else + getLogger().diag("GlfwWindow#setIcons » skipping null buffer"); + } + else + getLogger().diag("GlfwWindow#setIcons » icons is null"); + } + /** {@inheritDoc} */ @Override public void setSize(@NotNull Vec2i size) { diff --git a/windowing/glfw/src/main/java/module-info.java b/windowing/glfw/src/main/java/module-info.java index a19e85a4..405a0985 100644 --- a/windowing/glfw/src/main/java/module-info.java +++ b/windowing/glfw/src/main/java/module-info.java @@ -12,8 +12,8 @@ module sosengine.windowing.glfw { // -> Libraries requires transitive static lombok; requires transitive org.jetbrains.annotations; - requires org.lwjgl; requires org.lwjgl.glfw; + requires org.lwjgl.stb; // API access exports de.staropensource.engine.windowing.glfw; diff --git a/windowing/src/main/java/de/staropensource/engine/windowing/implementable/Window.java b/windowing/src/main/java/de/staropensource/engine/windowing/implementable/Window.java index 72ee6e03..7d4fd3a1 100644 --- a/windowing/src/main/java/de/staropensource/engine/windowing/implementable/Window.java +++ b/windowing/src/main/java/de/staropensource/engine/windowing/implementable/Window.java @@ -30,6 +30,7 @@ import lombok.Setter; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.nio.file.Path; import java.util.*; /** @@ -202,6 +203,27 @@ public abstract class Window implements AutoCloseable { @Setter private @NotNull String title; + /** + * Contains file paths to window + * icons in the PNG format. + * + * @since v1-alpha6 + * -- GETTER -- + * Returns file paths to window + * icons in the PNG format. + * + * @return file paths to icons + * @since v1-alpha6 + * -- SETTER -- + * Sets file paths to window + * icons in the PNG format. + * + * @param icons new file paths to icons + * @since v1-alpha6 + */ + @Setter + private @NotNull Path @Nullable [] icons; + /** * Contains the size of this window. * @@ -470,6 +492,7 @@ public abstract class Window implements AutoCloseable { * * @param name name * @param title title + * @param icons icon file paths * @param size size * @param minimumSize minimum size * @param maximumSize maximum size @@ -485,11 +508,12 @@ public abstract class Window implements AutoCloseable { * @throws Exception stuff thrown by the {@link #initializeWindow()} and {@link #render()} methods of the implementing windowing API * @since v1-alpha2 */ - protected Window(@NotNull String name, @NotNull String title, @NotNull Vec2i size, @NotNull Vec2i minimumSize, @NotNull Vec2i maximumSize, @NotNull Vec2i position, @NotNull WindowMode windowMode, @NotNull Monitor monitor, boolean resizable, boolean borderless, boolean focusable, boolean onTop, boolean transparent, boolean rendering) throws Exception { + protected Window(@NotNull String name, @NotNull String title, @NotNull Path @Nullable [] icons, @NotNull Vec2i size, @NotNull Vec2i minimumSize, @NotNull Vec2i maximumSize, @NotNull Vec2i position, @NotNull WindowMode windowMode, @NotNull Monitor monitor, boolean resizable, boolean borderless, boolean focusable, boolean onTop, boolean transparent, boolean rendering) throws Exception { // Initialize variables this.uniqueIdentifier = UUID.randomUUID(); this.name = name; this.title = title; + this.icons = icons; this.size = size; this.minimumSize = minimumSize; this.maximumSize = maximumSize; @@ -504,7 +528,7 @@ public abstract class Window implements AutoCloseable { this.rendering = rendering; // Log about window creation - logger.diag("Creating new window with properties: uniqueIdentifier=" + uniqueIdentifier + " name=\"" + name + "\" title=\"" + title + "\" size=" + size + " minimumSize=" + minimumSize + " maximumSize=" + maximumSize + " position=" + position + " windowMode=" + windowMode + " monitor=" + monitor.getUniqueIdentifier() + " (" + monitor.getName() + ") resizable=" + resizable + " borderless=" + borderless + " focusable=" + focusable + " onTop=" + onTop + " transparent=" + transparent + " rendering=" + rendering); + logger.diag("Creating new window with properties: uniqueIdentifier=" + uniqueIdentifier + " name=\"" + name + "\" title=\"" + title + "\" icons=" + Arrays.toString(icons) + " size=" + size + " minimumSize=" + minimumSize + " maximumSize=" + maximumSize + " position=" + position + " windowMode=" + windowMode + " monitor=" + monitor.getUniqueIdentifier() + " (" + monitor.getName() + ") resizable=" + resizable + " borderless=" + borderless + " focusable=" + focusable + " onTop=" + onTop + " transparent=" + transparent + " rendering=" + rendering); // Allow windowing API to initialize window initializeWindow(); @@ -627,6 +651,14 @@ public abstract class Window implements AutoCloseable { */ private @Nullable String title = null; + /** + * Contains the window icons in the PNG format. + * + * @see Window#icons + * @since v1-alpha6 + */ + private @NotNull Path @Nullable [] icons = null; + /** * Contains the window size. * @@ -783,8 +815,8 @@ public abstract class Window implements AutoCloseable { // Create new Window instance return WindowingSubsystem.getInstance().getApi().getInternalApi().getWindowClass() - .getDeclaredConstructor(String.class, String.class, Vec2i.class, Vec2i.class, Vec2i.class, Vec2i.class, WindowMode.class, Monitor.class, Boolean.TYPE, Boolean.TYPE, Boolean.TYPE, Boolean.TYPE, Boolean.TYPE, Boolean.TYPE) - .newInstance(name, title, size, minimumSize, maximumSize, position, windowMode, monitor, resizableBoolean, borderlessBoolean, focusableBoolean, onTopBoolean, transparentBoolean, renderingBoolean); + .getDeclaredConstructor(String.class, String.class, Path[].class, Vec2i.class, Vec2i.class, Vec2i.class, Vec2i.class, WindowMode.class, Monitor.class, Boolean.TYPE, Boolean.TYPE, Boolean.TYPE, Boolean.TYPE, Boolean.TYPE, Boolean.TYPE) + .newInstance(name, title, icons, size, minimumSize, maximumSize, position, windowMode, monitor, resizableBoolean, borderlessBoolean, focusableBoolean, onTopBoolean, transparentBoolean, renderingBoolean); } /** @@ -809,6 +841,18 @@ public abstract class Window implements AutoCloseable { return title; } + /** + * Returns file paths to window + * icons in the PNG format. + * + * @return file paths to icons + * @see Window#icons + * @since v1-alpha6 + */ + public @NotNull Path @Nullable [] getIcons() { + return icons; + } + /** * Returns the window size. * @@ -967,6 +1011,20 @@ public abstract class Window implements AutoCloseable { return this; } + /** + * Sets file paths to window + * icons in the PNG format. + * + * @param icons new file paths to icons + * @return builder instance + * @see Window#icons + * @since v1-alpha6 + */ + public @NotNull Builder setIcons(@NotNull Path @Nullable [] icons) { + this.icons = icons; + return this; + } + /** * Sets the window size. *