diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c4e2681 --- /dev/null +++ b/.gitignore @@ -0,0 +1,63 @@ +## Java + +*.class +*.war +*.ear +hs_err_pid* + +## Android Studio and Intellij and Android in general +.idea/ +*.ipr +*.iws +*.iml +com_crashlytics_export_strings.xml + +## Eclipse + +.classpath +.project +.metadata/ +/core/bin/ +/desktop/bin/ +*.tmp +*.bak +*.swp +*~.nib +.settings/ +.loadpath +.externalToolBuilders/ +*.launch + +## NetBeans + +/nbproject/private/ +/core/nbproject/private/ +/desktop/nbproject/private/ + +/build/ +/core/build/ +/desktop/build/ + +/nbbuild/ +/core/nbbuild/ +/desktop/nbbuild/ + +/dist/ +/core/dist/ +/desktop/dist/ + +/nbdist/ +/core/nbdist/ +/desktop/nbdist/ + +nbactions.xml +nb-configuration.xml + +## OS Specific +.DS_Store +Thumbs.db + +## SPCK Specific +build +.gradle +target \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e1856f2 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# Project Spck + +```bash +# Run from command line: +$ ./gradlew run +``` + +```bash +# Package +$ ./gradlew jlink +``` \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..ada2bcb --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + id("java") + id("org.javamodularity.moduleplugin").version("1.7.0").apply(false) +} + +allprojects { + repositories { + mavenCentral() + maven("https://oss.sonatype.org/content/repositories/snapshots/") + } +} + +subprojects { + apply(plugin = "org.javamodularity.moduleplugin") + group = "spck" + version = "1.0.0" + + java { + sourceCompatibility = JavaVersion.VERSION_16 + targetCompatibility = JavaVersion.VERSION_16 + } +} \ No newline at end of file diff --git a/core/build.gradle.kts b/core/build.gradle.kts new file mode 100644 index 0000000..e239e6f --- /dev/null +++ b/core/build.gradle.kts @@ -0,0 +1,60 @@ +import org.gradle.nativeplatform.platform.internal.ArchitectureInternal +import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform +import org.gradle.nativeplatform.platform.internal.DefaultOperatingSystem + +plugins { + id("java-library") +} + +val lwjglVersion = "3.3.0-SNAPSHOT" +val jomlVersion = "1.10.1" +val slf4jVersion = "1.8.0-beta4" +val resourcesPath = "../resources" + +val currentOs: DefaultOperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem() +val currentArch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture() +logger.quiet("[SPCK] System: ${currentOs.displayName} / ${currentArch.displayName}") + +fun specifyLwjglNatives(): String { + val currentOs = DefaultNativePlatform.getCurrentOperatingSystem() + val currentArch = DefaultNativePlatform.getCurrentArchitecture() + when { + currentOs.isMacOsX -> { + if(currentArch.displayName.contains("aarch64")) { + return "natives-macos-arm64" + } + return "natives-macos" + } + currentOs.isWindows -> { + if(currentArch.displayName.contains("aarch64")) { + return "natives-windows-arm64" + } + return "natives-windows" + } + currentOs.isLinux -> { + return "natives-linux" + } + else -> throw Error("Unrecognized or unsupported Operating system.") + } +} + +val lwjglNatives = specifyLwjglNatives() + +logger.quiet("[SPCK] Natives in use: $lwjglNatives") +logger.quiet("[SPCK] Resources path: $resourcesPath") + +sourceSets.main.get().resources.srcDirs(resourcesPath) + +dependencies { + api(platform("org.lwjgl:lwjgl-bom:$lwjglVersion")) + + implementation("org.lwjgl", "lwjgl") + api("org.lwjgl", "lwjgl-glfw") + implementation("org.lwjgl", "lwjgl-opengl") + implementation("org.lwjgl", "lwjgl", classifier = lwjglNatives) + implementation("org.lwjgl", "lwjgl-glfw", classifier = lwjglNatives) + implementation("org.lwjgl", "lwjgl-opengl", classifier = lwjglNatives) + implementation("org.joml", "joml", jomlVersion) + implementation("org.slf4j", "slf4j-api", slf4jVersion) + implementation("org.slf4j", "slf4j-simple", slf4jVersion) +} \ No newline at end of file diff --git a/core/src/main/java/module-info.java b/core/src/main/java/module-info.java new file mode 100644 index 0000000..e0b6a17 --- /dev/null +++ b/core/src/main/java/module-info.java @@ -0,0 +1,15 @@ +module spck.core { + requires org.lwjgl; + requires org.lwjgl.natives; + requires org.lwjgl.opengl; + requires org.lwjgl.opengl.natives; + requires transitive org.lwjgl.glfw; + requires org.lwjgl.glfw.natives; + requires org.joml; + requires org.slf4j; + + exports spck.core; + exports spck.core.input; + exports spck.core.props; + exports spck.core.props.type; +} \ No newline at end of file diff --git a/core/src/main/java/spck/core/Application.java b/core/src/main/java/spck/core/Application.java new file mode 100644 index 0000000..532d704 --- /dev/null +++ b/core/src/main/java/spck/core/Application.java @@ -0,0 +1,61 @@ +package spck.core; + +import org.joml.Vector4f; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import spck.core.props.Preferences; +import spck.core.render.Renderer; +import spck.core.window.Window; + +public class Application { + private static final Logger log = LoggerFactory.getLogger(Application.class); + private final Window window; + private final LifeCycleListener lifeCycleListener; + private final Renderer renderer; + + public Application(LifeCycleListener lifeCycleListener, Preferences preferences) { + if(Spck.app != null) { + throw new SpckRuntimeException("Cannot run multiple applications"); + } + Spck.app = this; + this.lifeCycleListener = lifeCycleListener; + renderer = new Renderer(preferences.renderBackend, preferences.debug); + window = new Window(preferences, renderer); + } + + public void start() { + log.info("Starting up application..."); + + window.create(); + lifeCycleListener.onCreated(); + + loop(); + + log.info("Application is shutting down"); + renderer.dispose(); + window.dispose(); + log.info("Terminated"); + } + + public void stop() { + window.close(); + } + + private void loop() { + long lastTime = System.nanoTime(); + + renderer.setClearColor(new Vector4f(1f, 1f, 1f, 1f)); + renderer.setClearFlags(Spck.types.bufferBit.color); + + while (!window.shouldClose()) { + renderer.clear(); + long now = System.nanoTime(); + long frameTimeNanos = now - lastTime; + float frameTime = frameTimeNanos / 1_000_000_000f; + lastTime = now; + + renderer.swapBuffers(frameTime); + window.pollEvents(); + } + } +} diff --git a/core/src/main/java/spck/core/Disposable.java b/core/src/main/java/spck/core/Disposable.java new file mode 100644 index 0000000..50b051e --- /dev/null +++ b/core/src/main/java/spck/core/Disposable.java @@ -0,0 +1,5 @@ +package spck.core; + +public interface Disposable { + void dispose(); +} diff --git a/core/src/main/java/spck/core/LifeCycleListener.java b/core/src/main/java/spck/core/LifeCycleListener.java new file mode 100644 index 0000000..de30cb0 --- /dev/null +++ b/core/src/main/java/spck/core/LifeCycleListener.java @@ -0,0 +1,5 @@ +package spck.core; + +public interface LifeCycleListener { + default void onCreated() {} +} diff --git a/core/src/main/java/spck/core/Spck.java b/core/src/main/java/spck/core/Spck.java new file mode 100644 index 0000000..e76830b --- /dev/null +++ b/core/src/main/java/spck/core/Spck.java @@ -0,0 +1,10 @@ +package spck.core; + +import spck.core.input.InputManager; +import spck.core.props.type.Types; + +public class Spck { + public static final InputManager input = new InputManager(); + public static Application app; + public static Types types; +} diff --git a/core/src/main/java/spck/core/SpckRuntimeException.java b/core/src/main/java/spck/core/SpckRuntimeException.java new file mode 100644 index 0000000..f3ae9ac --- /dev/null +++ b/core/src/main/java/spck/core/SpckRuntimeException.java @@ -0,0 +1,7 @@ +package spck.core; + +public class SpckRuntimeException extends RuntimeException { + public SpckRuntimeException(String message) { + super(message); + } +} diff --git a/core/src/main/java/spck/core/input/InputManager.java b/core/src/main/java/spck/core/input/InputManager.java new file mode 100644 index 0000000..48926f7 --- /dev/null +++ b/core/src/main/java/spck/core/input/InputManager.java @@ -0,0 +1,15 @@ +package spck.core.input; + +public class InputManager { + private InputMultiplexer multiplexer; + + public void setMultiplexer(InputMultiplexer multiplexer) { + this.multiplexer = multiplexer; + } + + public void keyCallback(int key, int scancode, int action, int mods) { + if(multiplexer != null) { + multiplexer.keyCallback(key, scancode, action, mods); + } + } +} diff --git a/core/src/main/java/spck/core/input/InputMultiplexer.java b/core/src/main/java/spck/core/input/InputMultiplexer.java new file mode 100644 index 0000000..a42ade8 --- /dev/null +++ b/core/src/main/java/spck/core/input/InputMultiplexer.java @@ -0,0 +1,34 @@ +package spck.core.input; + +import java.util.ArrayList; +import java.util.List; + +import static org.lwjgl.glfw.GLFW.*; + +public class InputMultiplexer { + private final List processors = new ArrayList<>(); + + public int addProcessor(InputProcessor processor) { + processors.add(processor); + return processors.size() - 1; + } + + public void removeProcessor(int index) { + processors.remove(index); + } + + public void keyCallback(int key, int scancode, int action, int mods) { + switch (action) { + case GLFW_PRESS: + for (InputProcessor processor : processors) { + processor.onKeyPress(key); + } + break; + case GLFW_RELEASE: + for (InputProcessor processor : processors) { + processor.onKeyReleased(key); + } + break; + } + } +} diff --git a/core/src/main/java/spck/core/input/InputProcessor.java b/core/src/main/java/spck/core/input/InputProcessor.java new file mode 100644 index 0000000..ca2d74c --- /dev/null +++ b/core/src/main/java/spck/core/input/InputProcessor.java @@ -0,0 +1,7 @@ +package spck.core.input; + +public interface InputProcessor { + default void onKeyPress(int keyCode) {} + + default void onKeyReleased(int keyCode) {} +} diff --git a/core/src/main/java/spck/core/props/Antialiasing.java b/core/src/main/java/spck/core/props/Antialiasing.java new file mode 100644 index 0000000..3238851 --- /dev/null +++ b/core/src/main/java/spck/core/props/Antialiasing.java @@ -0,0 +1,20 @@ +package spck.core.props; + +public enum Antialiasing { + OFF(0), + ANTIALISING_2X(2), + ANTIALISING_4X(4), + ANTIALISING_8X(8), + ANTIALISING_16X(16); + + private int value; + + Antialiasing(int value) { + this.value = value; + } + + public int getValue() { + return value; + } +} + diff --git a/core/src/main/java/spck/core/props/Preferences.java b/core/src/main/java/spck/core/props/Preferences.java new file mode 100644 index 0000000..d8ed5f3 --- /dev/null +++ b/core/src/main/java/spck/core/props/Preferences.java @@ -0,0 +1,17 @@ +package spck.core.props; + +import org.joml.Vector2i; + +public class Preferences { + public enum RendererBackend { + OPENGL, + } + + public String title = "Project SPCK"; + public RendererBackend renderBackend = RendererBackend.OPENGL; + public Antialiasing antialiasing = Antialiasing.OFF; + public Vector2i windowSize = new Vector2i(1280, 720); + public boolean vSync = false; + public boolean fullscreen = false; + public boolean debug = false; +} diff --git a/core/src/main/java/spck/core/props/type/BufferBit.java b/core/src/main/java/spck/core/props/type/BufferBit.java new file mode 100644 index 0000000..c1e8f9d --- /dev/null +++ b/core/src/main/java/spck/core/props/type/BufferBit.java @@ -0,0 +1,13 @@ +package spck.core.props.type; + +public class BufferBit { + public final int color; + public final int depth; + public final int stencil; + + public BufferBit(int color, int depth, int stencil) { + this.color = color; + this.depth = depth; + this.stencil = stencil; + } +} diff --git a/core/src/main/java/spck/core/props/type/Types.java b/core/src/main/java/spck/core/props/type/Types.java new file mode 100644 index 0000000..b1a4dde --- /dev/null +++ b/core/src/main/java/spck/core/props/type/Types.java @@ -0,0 +1,9 @@ +package spck.core.props.type; + +public class Types { + public final BufferBit bufferBit; + + public Types(BufferBit bufferBit) { + this.bufferBit = bufferBit; + } +} diff --git a/core/src/main/java/spck/core/render/GraphicsContext.java b/core/src/main/java/spck/core/render/GraphicsContext.java new file mode 100644 index 0000000..23990af --- /dev/null +++ b/core/src/main/java/spck/core/render/GraphicsContext.java @@ -0,0 +1,20 @@ +package spck.core.render; + +import org.joml.Vector4f; +import spck.core.Disposable; + +public interface GraphicsContext extends Disposable { + void windowPrepared(); + + void windowCreated(long windowId, int windowWidth, int windowHeight, boolean vSync); + + void setClearFlags(int mask); + + void setClearColor(Vector4f color); + + void clear(); + + void swapBuffers(float frameTime); + + void windowResized(int newWidth, int newHeight); +} diff --git a/core/src/main/java/spck/core/render/Renderer.java b/core/src/main/java/spck/core/render/Renderer.java new file mode 100644 index 0000000..2298e99 --- /dev/null +++ b/core/src/main/java/spck/core/render/Renderer.java @@ -0,0 +1,62 @@ +package spck.core.render; + +import org.joml.Vector4f; +import spck.core.Spck; +import spck.core.props.Preferences; +import spck.core.SpckRuntimeException; +import spck.core.vendor.opengl.OpenGLTypes; +import spck.core.vendor.opengl.OpenGLGraphicsContext; +import spck.core.window.WindowEventListener; + +public class Renderer implements WindowEventListener { + private final GraphicsContext context; + private final Preferences.RendererBackend backend; // TODO + + public Renderer(Preferences.RendererBackend backend, boolean debug) { + this.backend = backend; + + switch (backend) { + case OPENGL: + context = new OpenGLGraphicsContext(debug); + Spck.types = new OpenGLTypes(); + break; + default: + throw new SpckRuntimeException("Only OPENGL backend is supported"); + } + } + + @Override + public void onWindowPrepared() { + context.windowPrepared(); + } + + @Override + public void onWindowCreated(long windowId, int width, int height, boolean vSync) { + context.windowCreated(windowId, width, height, vSync); + } + + @Override + public void onWindowResized(int width, int height) { + context.windowResized(width, height); + } + + public void setClearColor(Vector4f color) { + context.setClearColor(color); + } + + public void setClearFlags(int mask) { + context.setClearFlags(mask); + } + + public void swapBuffers(float frameTime) { + context.swapBuffers(frameTime); + } + + public void clear() { + context.clear(); + } + + public void dispose() { + context.dispose(); + } +} diff --git a/core/src/main/java/spck/core/vendor/opengl/OpenGLBufferBit.java b/core/src/main/java/spck/core/vendor/opengl/OpenGLBufferBit.java new file mode 100644 index 0000000..ce941e1 --- /dev/null +++ b/core/src/main/java/spck/core/vendor/opengl/OpenGLBufferBit.java @@ -0,0 +1,15 @@ +package spck.core.vendor.opengl; + +import spck.core.props.type.BufferBit; + +import static org.lwjgl.opengl.GL41.*; + +public class OpenGLBufferBit extends BufferBit { + public OpenGLBufferBit() { + super( + GL_COLOR_BUFFER_BIT, + GL_DEPTH_BUFFER_BIT, + GL_STENCIL_BUFFER_BIT + ); + } +} diff --git a/core/src/main/java/spck/core/vendor/opengl/OpenGLGraphicsContext.java b/core/src/main/java/spck/core/vendor/opengl/OpenGLGraphicsContext.java new file mode 100644 index 0000000..d79ec47 --- /dev/null +++ b/core/src/main/java/spck/core/vendor/opengl/OpenGLGraphicsContext.java @@ -0,0 +1,85 @@ +package spck.core.vendor.opengl; + +import org.joml.Vector4f; +import org.lwjgl.opengl.GLUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import spck.core.render.GraphicsContext; + +import static org.lwjgl.glfw.GLFW.*; +import static org.lwjgl.opengl.GL.createCapabilities; +import static org.lwjgl.opengl.GL41.*; + +public class OpenGLGraphicsContext implements GraphicsContext { + private static final Logger log = LoggerFactory.getLogger(OpenGLGraphicsContext.class); + private final boolean debug; + private int clearFlags = 0; + private long windowId; + + public OpenGLGraphicsContext(boolean debug) { + this.debug = debug; + } + + @Override + public void windowPrepared() { + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); + glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); + if(debug){ + glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GLFW_TRUE); + } + } + + @Override + public void windowCreated(long windowId, int windowWidth, int windowHeight, boolean vSync) { + log.debug("Initializing OpenGL context..."); + this.windowId = windowId; + + glfwMakeContextCurrent(this.windowId); + + createCapabilities(); + + if (debug) { + GLUtil.setupDebugMessageCallback(); + } + + log.debug("Initialized"); + log.debug("\tOpenGL Vendor: {}", glGetString(GL_VENDOR)); + log.debug("\tVersion: {}", glGetString(GL_VERSION)); + log.debug("\tRenderer: {}", glGetString(GL_RENDERER)); + log.debug("\tShading Language Version: {}", glGetString(GL_SHADING_LANGUAGE_VERSION)); + log.debug("\tVsync: {}", vSync); + + glfwSwapInterval(vSync ? GLFW_TRUE : GLFW_FALSE); + } + + @Override + public void setClearFlags(int mask) { + this.clearFlags = mask; + } + + @Override + public void setClearColor(Vector4f color) { + glClearColor(color.x, color.y, color.z, color.w); + } + + @Override + public void clear() { + glClear(clearFlags); + } + + @Override + public void swapBuffers(float frameTime) { + glfwSwapBuffers(windowId); + } + + @Override + public void windowResized(int newWidth, int newHeight) { + glViewport(0, 0, newWidth, newHeight); + } + + @Override + public void dispose() { + } +} diff --git a/core/src/main/java/spck/core/vendor/opengl/OpenGLTypes.java b/core/src/main/java/spck/core/vendor/opengl/OpenGLTypes.java new file mode 100644 index 0000000..d2313dd --- /dev/null +++ b/core/src/main/java/spck/core/vendor/opengl/OpenGLTypes.java @@ -0,0 +1,9 @@ +package spck.core.vendor.opengl; + +import spck.core.props.type.Types; + +public class OpenGLTypes extends Types { + public OpenGLTypes() { + super(new OpenGLBufferBit()); + } +} diff --git a/core/src/main/java/spck/core/window/Window.java b/core/src/main/java/spck/core/window/Window.java new file mode 100644 index 0000000..33e079b --- /dev/null +++ b/core/src/main/java/spck/core/window/Window.java @@ -0,0 +1,168 @@ +package spck.core.window; + +import org.joml.Vector2i; +import org.lwjgl.glfw.GLFWErrorCallback; +import org.lwjgl.glfw.GLFWVidMode; +import org.lwjgl.system.MemoryUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import spck.core.Spck; +import spck.core.props.Antialiasing; +import spck.core.Disposable; +import spck.core.props.Preferences; + +import java.nio.IntBuffer; +import java.util.Objects; + +import static org.lwjgl.glfw.Callbacks.glfwFreeCallbacks; +import static org.lwjgl.glfw.GLFW.*; +import static org.lwjgl.system.MemoryUtil.NULL; + +public class Window implements Disposable { + private static final Logger log = LoggerFactory.getLogger(Window.class); + private final Vector2i size = new Vector2i(); + private final Preferences preferences; + private final WindowEventListener listener; + private GLFWVidMode videoMode; + private long id; + private int screenPixelRatio; + + public Window(Preferences preferences, WindowEventListener listener) { + this.preferences = preferences; + this.listener = listener; + size.set(preferences.windowSize); + } + + public void create() { + log.debug("Creating window..."); + + if (!glfwInit()) { + throw new RuntimeException("Error initializing GLFW"); + } + + glfwSetErrorCallback(GLFWErrorCallback.createPrint(System.err)); + + glfwDefaultWindowHints(); + glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); + glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE); + + videoMode = pickMonitor(); + + if (preferences.antialiasing != Antialiasing.OFF) { + log.debug("Antialiasing: {}", preferences.antialiasing.getValue()); + glfwWindowHint(GLFW_SAMPLES, preferences.antialiasing.getValue()); + } + + if (preferences.fullscreen) { + size.set(videoMode.width(), videoMode.height()); + glfwWindowHint(GLFW_MAXIMIZED, GLFW_TRUE); + } + + listener.onWindowPrepared(); + + id = glfwCreateWindow(size.x, size.y, preferences.title, preferences.fullscreen ? glfwGetPrimaryMonitor() : NULL, NULL); + + if (id == NULL) { + throw new RuntimeException("Error creating GLFW window"); + } + + listener.onWindowCreated(id, size.x, size.y, preferences.vSync); + + glfwSetFramebufferSizeCallback(id, this::framebufferResized); + calculateScreenPixelRatio(); + + log.debug("Creating input"); + //input.initialize(new Input.InitializationParams(size.x, windowSize.y, this::cursorPositionHasChanged)); + + log.debug("Setting up input callbacks"); + glfwSetKeyCallback(id, (window, key, scancode, action, mods) -> Spck.input.keyCallback(key, scancode, action, mods)); + /*glfwSetCursorPosCallback(id, (window, x, y) -> input.cursorPosCallback(x, y)); + glfwSetScrollCallback(id, (window, xOffset, yOffset) -> input.mouseScrollCallback(xOffset, yOffset)); + glfwSetMouseButtonCallback(id, (window, button, action, mods) -> input.mouseButtonCallback(button, action, mods)); + */ + + if (!preferences.fullscreen) { + glfwSetWindowPos(id, (videoMode.width() - size.x) / 2, (videoMode.height() - size.y) / 2); + } + + log.debug("Window has been setup"); + glfwShowWindow(id); + } + + public void close() { + glfwSetWindowShouldClose(id, true); + } + + public boolean shouldClose() { + return glfwWindowShouldClose(id); + } + + public void pollEvents() { + glfwPollEvents(); + } + + @Override + public void dispose() { + glfwFreeCallbacks(id); + glfwDestroyWindow(id); + glfwTerminate(); + Objects.requireNonNull(glfwSetErrorCallback(null)).free(); + } + + private GLFWVidMode pickMonitor() { + final var buffer = glfwGetMonitors(); + + if (buffer == null) { + throw new RuntimeException("No monitors were found"); + } + + if (buffer.capacity() == 1) { + log.info("Found one monitor: {}", glfwGetMonitorName(buffer.get())); + } else { + log.info("Found multiple monitors:"); + for (int i = 0; i < buffer.capacity(); i++) { + log.info(" Monitor-{} '{}'", i, glfwGetMonitorName(buffer.get(i))); + } + } + + return glfwGetVideoMode(glfwGetPrimaryMonitor()); + } + + private void framebufferResized(long window, int width, int height) { + size.set(width, height); + listener.onWindowResized(width, height); + calculateScreenPixelRatio(); + log.debug("Framebuffer size change to {}x{}", width, height); + } + + private void calculateScreenPixelRatio() { + // https://en.wikipedia.org/wiki/4K_resolution + int uhdMinWidth = 3840; + int uhdMinHeight = 1716; + boolean UHD = videoMode.width() >= uhdMinWidth && videoMode.height() >= uhdMinHeight; + log.debug("Screen is {}x{}, UHD: {}", videoMode.width(), videoMode.height(), UHD); + + // Check if the monitor is 4K + if (UHD) { + screenPixelRatio = 2; + log.debug("Screen pixel ratio has been set to: {}", screenPixelRatio); + return; + } + + IntBuffer widthScreenCoordBuf = MemoryUtil.memAllocInt(1); + IntBuffer heightScreenCoordBuf = MemoryUtil.memAllocInt(1); + IntBuffer widthPixelsBuf = MemoryUtil.memAllocInt(1); + IntBuffer heightPixelsBuf = MemoryUtil.memAllocInt(1); + + glfwGetWindowSize(id, widthScreenCoordBuf, heightScreenCoordBuf); + glfwGetFramebufferSize(id, widthPixelsBuf, heightPixelsBuf); + + screenPixelRatio = (int) Math.floor((float) widthPixelsBuf.get() / (float) widthScreenCoordBuf.get()); + log.debug("Screen pixel ratio has been set to: {}", screenPixelRatio); + + MemoryUtil.memFree(widthScreenCoordBuf); + MemoryUtil.memFree(heightScreenCoordBuf); + MemoryUtil.memFree(widthPixelsBuf); + MemoryUtil.memFree(heightPixelsBuf); + } +} diff --git a/core/src/main/java/spck/core/window/WindowEventListener.java b/core/src/main/java/spck/core/window/WindowEventListener.java new file mode 100644 index 0000000..91ef3b7 --- /dev/null +++ b/core/src/main/java/spck/core/window/WindowEventListener.java @@ -0,0 +1,9 @@ +package spck.core.window; + +public interface WindowEventListener { + void onWindowPrepared(); + + void onWindowCreated(long windowId, int width, int height, boolean vSync); + + void onWindowResized(int width, int height); +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..f371643 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/resources/simplelogger.properties b/resources/simplelogger.properties new file mode 100644 index 0000000..1bd08fb --- /dev/null +++ b/resources/simplelogger.properties @@ -0,0 +1,6 @@ +org.slf4j.simpleLogger.showDateTime=true +org.slf4j.simpleLogger.showThreadName=true +org.slf4j.simpleLogger.showLogName=true +org.slf4j.simpleLogger.levelInBrackets=true +org.slf4j.simpleLogger.dateTimeFormat=HH:mm:ss.SSS +org.slf4j.simpleLogger.defaultLogLevel=DEBUG \ No newline at end of file diff --git a/sandbox/build.gradle.kts b/sandbox/build.gradle.kts new file mode 100644 index 0000000..b630d6b --- /dev/null +++ b/sandbox/build.gradle.kts @@ -0,0 +1,49 @@ +import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform +import org.gradle.nativeplatform.platform.internal.DefaultOperatingSystem + +plugins { + id("java") + id("application") + id("org.beryx.jlink").version("2.23.8") +} + +dependencies { + implementation(project(":core")) +} + +val currentOs: DefaultOperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem() +var spckJvmArgs = listOf("-Dorg.lwjgl.system.allocator=system", "-Dorg.lwjgl.util.DebugLoader=true", "-Dorg.lwjgl.util.Debug=true", "-Dorg.lwjgl.opengl.Display.enableHighDPI=true", "-Dorg.lwjgl.opengl.Display.enableOSXFullscreenModeAPI=true") +if(currentOs.isMacOsX) { + spckJvmArgs = spckJvmArgs.plus(listOf("-XstartOnFirstThread")) +} + +application { + mainModule.set(moduleName) + mainClass.set("spck.sandbox.Sandbox") + applicationDefaultJvmArgs = spckJvmArgs + + logger.quiet("[SPCK] [application] module: ${mainModule.get()}") + logger.quiet("[SPCK] [application] class: ${mainClass.get()}") + logger.quiet("[SPCK] [application] JVM args: ${spckJvmArgs.joinToString(", ")}") + + // See https://github.com/java9-modularity/gradle-modules-plugin/issues/165 + modularity.disableEffectiveArgumentsAdjustment() +} + +jlink { + addOptions("--strip-debug", "--compress", "2", "--no-header-files", "--no-man-pages") + // https://github.com/beryx-gist/badass-jlink-example-richtextfx/blob/master/build.gradle + // jvmArgs = ['-splash:$APPDIR/splash.png'] + + logger.quiet("[SPCK] [jlink] Options: ${jlink.options.get().joinToString(" ")}") + logger.quiet("[SPCK] [jlink] JVM args: ${spckJvmArgs.joinToString(", ")}") + + launcher { + name = "spck-app" + jvmArgs = spckJvmArgs + } + + jpackage { + skipInstaller = true + } +} diff --git a/sandbox/src/main/java/module-info.java b/sandbox/src/main/java/module-info.java new file mode 100644 index 0000000..04c1bb8 --- /dev/null +++ b/sandbox/src/main/java/module-info.java @@ -0,0 +1,3 @@ +module spck.sandbox { + requires spck.core; +} \ No newline at end of file diff --git a/sandbox/src/main/java/spck/sandbox/Sandbox.java b/sandbox/src/main/java/spck/sandbox/Sandbox.java new file mode 100644 index 0000000..b4f407f --- /dev/null +++ b/sandbox/src/main/java/spck/sandbox/Sandbox.java @@ -0,0 +1,12 @@ +package spck.sandbox; + +import spck.core.Application; +import spck.core.props.Preferences; + +public class Sandbox { + public static void main(String[] args) { + final var prefs = new Preferences(); + + new Application(new SandboxGame(), prefs).start(); + } +} diff --git a/sandbox/src/main/java/spck/sandbox/SandboxGame.java b/sandbox/src/main/java/spck/sandbox/SandboxGame.java new file mode 100644 index 0000000..5700d9f --- /dev/null +++ b/sandbox/src/main/java/spck/sandbox/SandboxGame.java @@ -0,0 +1,24 @@ +package spck.sandbox; + +import spck.core.LifeCycleListener; +import spck.core.Spck; +import spck.core.input.InputMultiplexer; +import spck.core.input.InputProcessor; +import static org.lwjgl.glfw.GLFW.*; + +public class SandboxGame implements LifeCycleListener, InputProcessor { + private final InputMultiplexer inputMultiplexer = new InputMultiplexer(); + + @Override + public void onCreated() { + inputMultiplexer.addProcessor(this); + Spck.input.setMultiplexer(inputMultiplexer); + } + + @Override + public void onKeyPress(int keyCode) { + if(keyCode == GLFW_KEY_ESCAPE) { + Spck.app.stop(); + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..77fd514 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,2 @@ +include("core", "sandbox") +rootProject.name = "project-spck" \ No newline at end of file