From 8646ca1e35d7ad9cdf358bee44064b5c5b44a6b5 Mon Sep 17 00:00:00 2001 From: rudrabeniwal Date: Wed, 9 Apr 2025 14:55:27 +0530 Subject: [PATCH] Implement GLFW window system with event handling and Vulkan support --- build.sbt | 5 +- .../computenode/cyfra/window/GLFWSystem.scala | 43 ++++ .../cyfra/window/GLFWWindowSystem.scala | 217 ++++++++++++++++++ .../cyfra/window/WindowHandle.scala | 84 +++++++ .../cyfra/window/WindowSystem.scala | 31 +++ .../cyfra/window/WindowSystemTest.scala | 102 ++++++++ 6 files changed, 481 insertions(+), 1 deletion(-) create mode 100644 src/main/scala/io/computenode/cyfra/window/GLFWSystem.scala create mode 100644 src/main/scala/io/computenode/cyfra/window/GLFWWindowSystem.scala create mode 100644 src/main/scala/io/computenode/cyfra/window/WindowHandle.scala create mode 100644 src/main/scala/io/computenode/cyfra/window/WindowSystem.scala create mode 100644 src/main/scala/io/computenode/cyfra/window/WindowSystemTest.scala diff --git a/build.sbt b/build.sbt index be3ae143..cc433562 100644 --- a/build.sbt +++ b/build.sbt @@ -44,6 +44,8 @@ lazy val root = (project in file(".")) "org.lwjgl" % "lwjgl-vma" % lwjglVersion, "org.lwjgl" % "lwjgl" % lwjglVersion classifier lwjglNatives, "org.lwjgl" % "lwjgl-vma" % lwjglVersion classifier lwjglNatives, + "org.lwjgl" % "lwjgl-glfw" % lwjglVersion, + "org.lwjgl" % "lwjgl-glfw" % lwjglVersion classifier lwjglNatives, "org.joml" % "joml" % jomlVersion, "commons-io" % "commons-io" % "2.16.1", "org.slf4j" % "slf4j-api" % "1.7.30", @@ -52,7 +54,8 @@ lazy val root = (project in file(".")) "org.junit.jupiter" % "junit-jupiter" % "5.6.2" % Test, "org.junit.jupiter" % "junit-jupiter-engine" % "5.7.2" % Test, "com.lihaoyi" %% "sourcecode" % "0.4.3-M5" - ) + ), + mainClass := Some("com.computenode.cyfra.app.Main") ) lazy val vulkanSdk = System.getenv("VULKAN_SDK") diff --git a/src/main/scala/io/computenode/cyfra/window/GLFWSystem.scala b/src/main/scala/io/computenode/cyfra/window/GLFWSystem.scala new file mode 100644 index 00000000..b0f85224 --- /dev/null +++ b/src/main/scala/io/computenode/cyfra/window/GLFWSystem.scala @@ -0,0 +1,43 @@ +package io.computenode.cyfra.window + +import org.lwjgl.glfw.{GLFW, GLFWErrorCallback} +import org.lwjgl.system.MemoryUtil.NULL +import org.lwjgl.glfw.GLFWVulkan.glfwVulkanSupported + +import scala.util.{Try, Success, Failure} + +/** + * GLFW window system implementation. + */ +object GLFWSystem { + /** + * Initializes GLFW with appropriate configuration for Vulkan rendering. + * + * @return Success if initialization was successful, Failure otherwise + */ + def initializeGLFW(): Try[Unit] = Try { + // Setup error callback + val errorCallback = GLFWErrorCallback.createPrint(System.err) + GLFW.glfwSetErrorCallback(errorCallback) + + // Initialize GLFW + if (!GLFW.glfwInit()) { + throw new RuntimeException("Failed to initialize GLFW") + } + + // Configure GLFW for Vulkan + GLFW.glfwWindowHint(GLFW.GLFW_CLIENT_API, GLFW.GLFW_NO_API) // No OpenGL context + GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE, GLFW.GLFW_TRUE) // Window resizable + + // Check Vulkan support + if (!glfwVulkanSupported()) { + throw new RuntimeException("GLFW: Vulkan is not supported") + } + + // Register shutdown hook to terminate GLFW + sys.addShutdownHook { + GLFW.glfwTerminate() + if (errorCallback != null) errorCallback.free() + } + } +} \ No newline at end of file diff --git a/src/main/scala/io/computenode/cyfra/window/GLFWWindowSystem.scala b/src/main/scala/io/computenode/cyfra/window/GLFWWindowSystem.scala new file mode 100644 index 00000000..40fc82f4 --- /dev/null +++ b/src/main/scala/io/computenode/cyfra/window/GLFWWindowSystem.scala @@ -0,0 +1,217 @@ +package io.computenode.cyfra.window + +import org.lwjgl.glfw.{GLFW, GLFWWindowSizeCallback, GLFWKeyCallback, GLFWMouseButtonCallback, + GLFWCursorPosCallback, GLFWFramebufferSizeCallback, GLFWWindowCloseCallback, + GLFWCharCallback, GLFWScrollCallback} +import org.lwjgl.system.MemoryUtil.NULL +import org.lwjgl.system.MemoryStack +import java.util.concurrent.ConcurrentLinkedQueue +import scala.jdk.CollectionConverters._ +import scala.util.{Try, Success, Failure} + +/** + * GLFW implementation of WindowSystem interface. + */ +class GLFWWindowSystem extends WindowSystem { + // Thread-safe event queue to collect events between polls + private val eventQueue = new ConcurrentLinkedQueue[WindowEvent]() + + // Initialize GLFW first + GLFWSystem.initializeGLFW() match { + case Failure(exception) => throw exception + case Success(_) => // GLFW initialized successfully + } + + /** + * Applies window hints for GLFW window creation. + * This method centralizes all window configuration options. + */ + private def applyWindowHints(): Unit = { + // Core window hints + GLFW.glfwWindowHint(GLFW.GLFW_CLIENT_API, GLFW.GLFW_NO_API) // No OpenGL context, using Vulkan + GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE, GLFW.GLFW_TRUE) // Window resizable + + // Platform-specific hints + val osName = System.getProperty("os.name").toLowerCase + + if (osName.contains("mac")) { + // macOS specific hints + GLFW.glfwWindowHint(GLFW.GLFW_COCOA_RETINA_FRAMEBUFFER, GLFW.GLFW_FALSE) + GLFW.glfwWindowHint(GLFW.GLFW_COCOA_GRAPHICS_SWITCHING, GLFW.GLFW_TRUE) + } else if (osName.contains("win")) { + // Windows-specific hints + GLFW.glfwWindowHint(GLFW.GLFW_SCALE_TO_MONITOR, GLFW.GLFW_TRUE) + } else if (osName.contains("linux") || osName.contains("unix")) { + // Linux specific hints + GLFW.glfwWindowHint(GLFW.GLFW_FOCUS_ON_SHOW, GLFW.GLFW_TRUE) + } + } + + /** + * Creates a new GLFW window with specified dimensions and title. + * + * @param width The width of the window in pixels + * @param height The height of the window in pixels + * @param title The window title + * @return A handle to the created window + */ + override def createWindow(width: Int, height: Int, title: String): WindowHandle = { + // Apply window hints before creating the window + applyWindowHints() + + // Create the window + val windowPtr = GLFW.glfwCreateWindow(width, height, title, NULL, NULL) + if (windowPtr == NULL) { + throw new RuntimeException("Failed to create GLFW window") + } + + val handle = new GLFWWindowHandle(windowPtr) + + // Register callbacks for this window + setupCallbacks(handle) + + // Position window in the center of the primary monitor + val vidMode = GLFW.glfwGetVideoMode(GLFW.glfwGetPrimaryMonitor()) + GLFW.glfwSetWindowPos( + windowPtr, + (vidMode.width() - width) / 2, + (vidMode.height() - height) / 2 + ) + + // Make the window visible + GLFW.glfwShowWindow(windowPtr) + + handle + } + + /** + * Polls for and returns pending window events. + * + * @return List of window events that occurred since the last poll + */ + override def pollEvents(): List[WindowEvent] = { + // Poll for events + GLFW.glfwPollEvents() + + // Drain the event queue to a list + val events = eventQueue.asScala.toList + eventQueue.clear() + events + } + + /** + * Checks if a window should close. + * + * @param window The window handle to check + * @return true if the window should close, false otherwise + */ + override def shouldWindowClose(window: WindowHandle): Boolean = { + GLFW.glfwWindowShouldClose(window.nativePtr) + } + + /** + * Sets up GLFW callbacks for the given window handle. + * Callbacks will populate the eventQueue with WindowEvents. + */ + private def setupCallbacks(window: WindowHandle): Unit = { + val windowPtr = window.nativePtr + + // Window framebuffer size callback (for handling DPI changes) + GLFW.glfwSetFramebufferSizeCallback(windowPtr, new GLFWFramebufferSizeCallback { + override def invoke(window: Long, width: Int, height: Int): Unit = { + eventQueue.add(WindowEvent.Resize(width, height)) + } + }) + + // Window size callback + GLFW.glfwSetWindowSizeCallback(windowPtr, new GLFWWindowSizeCallback { + override def invoke(window: Long, width: Int, height: Int): Unit = { + eventQueue.add(WindowEvent.Resize(width, height)) + } + }) + + // Key callback + GLFW.glfwSetKeyCallback(windowPtr, new GLFWKeyCallback { + override def invoke(window: Long, key: Int, scancode: Int, action: Int, mods: Int): Unit = { + eventQueue.add(WindowEvent.Key(key, action, mods)) + } + }) + + // Character input callback (for text input) + GLFW.glfwSetCharCallback(windowPtr, new GLFWCharCallback { + override def invoke(window: Long, codepoint: Int): Unit = { + eventQueue.add(WindowEvent.CharInput(codepoint)) + } + }) + + // Mouse button callback - FIX THE BUFFER ALLOCATION + GLFW.glfwSetMouseButtonCallback(windowPtr, new GLFWMouseButtonCallback { + override def invoke(window: Long, button: Int, action: Int, mods: Int): Unit = { + // Create the buffers within the scope of this function + val stack = MemoryStack.stackPush() + try { + val xBuffer = stack.mallocDouble(1) + val yBuffer = stack.mallocDouble(1) + GLFW.glfwGetCursorPos(window, xBuffer, yBuffer) + val x = xBuffer.get(0) + val y = yBuffer.get(0) + + eventQueue.add(WindowEvent.MouseButton(button, action == GLFW.GLFW_PRESS, x, y)) + } finally { + stack.pop() + } + } + }) + + // Cursor position callback + GLFW.glfwSetCursorPosCallback(windowPtr, new GLFWCursorPosCallback { + override def invoke(window: Long, x: Double, y: Double): Unit = { + eventQueue.add(WindowEvent.MouseMove(x, y)) + } + }) + + // Scroll callback + GLFW.glfwSetScrollCallback(windowPtr, new GLFWScrollCallback { + override def invoke(window: Long, xoffset: Double, yoffset: Double): Unit = { + eventQueue.add(WindowEvent.Scroll(xoffset, yoffset)) + } + }) + + // Set close callback + GLFW.glfwSetWindowCloseCallback(windowPtr, new GLFWWindowCloseCallback { + override def invoke(window: Long): Unit = { + eventQueue.add(WindowEvent.Close) + } + }) + } + + /** + * Example of how to handle specific events like resize and close. + * + * @param events List of events to handle + */ + def handleEvents(events: List[WindowEvent], window: WindowHandle): Unit = { + events.foreach { + case WindowEvent.Resize(width, height) => + // Handle resize event, e.g., update viewport + println(s"Window resized to ${width}x${height}") + + case WindowEvent.Close => + // Handle close event, e.g., clean up resources + println("Window close requested") + GLFW.glfwSetWindowShouldClose(window.nativePtr, true) + + case WindowEvent.Key(keyCode, action, mods) => + // Handle key events + val actionName = action match { + case GLFW.GLFW_PRESS => "pressed" + case GLFW.GLFW_RELEASE => "released" + case GLFW.GLFW_REPEAT => "repeated" + case _ => "unknown" + } + println(s"Key $keyCode was $actionName with modifiers $mods") + + case _ => // Ignore other events + } + } +} \ No newline at end of file diff --git a/src/main/scala/io/computenode/cyfra/window/WindowHandle.scala b/src/main/scala/io/computenode/cyfra/window/WindowHandle.scala new file mode 100644 index 00000000..e9c4e412 --- /dev/null +++ b/src/main/scala/io/computenode/cyfra/window/WindowHandle.scala @@ -0,0 +1,84 @@ +package io.computenode.cyfra.window + +/** + * Platform-agnostic handle to a window instance. + */ +trait WindowHandle { + /** + * Returns the native window pointer. + * For GLFW, this is the GLFWwindow pointer as a long value. + */ + def nativePtr: Long +} + +/** + * Implementation of WindowHandle for GLFW windows. + */ +class GLFWWindowHandle(val nativePtr: Long) extends WindowHandle { + // GLFW-specific window operations could be added here if needed +} + +/** + * Represents window-related events. + */ +sealed trait WindowEvent + +/** + * Common window events. + */ +object WindowEvent { + /** + * Window resize event. + * + * @param width The new width of the window + * @param height The new height of the window + */ + case class Resize(width: Int, height: Int) extends WindowEvent + + /** + * Key event. + * + * @param keyCode The GLFW key code + * @param action The action (press, release, repeat) + * @param mods Modifier keys that were held + */ + case class Key(keyCode: Int, action: Int, mods: Int) extends WindowEvent + + /** + * Character input event (for text input). + * + * @param codepoint Unicode code point of the character + */ + case class CharInput(codepoint: Int) extends WindowEvent + + /** + * Mouse movement event. + * + * @param x The x coordinate + * @param y The y coordinate + */ + case class MouseMove(x: Double, y: Double) extends WindowEvent + + /** + * Mouse button event. + * + * @param button The button number + * @param pressed True if pressed, false if released + * @param x The x coordinate + * @param y The y coordinate + */ + case class MouseButton(button: Int, pressed: Boolean, x: Double, y: Double) extends WindowEvent + + /** + * Scroll wheel event. + * + * @param xOffset Horizontal scroll amount + * @param yOffset Vertical scroll amount + */ + case class Scroll(xOffset: Double, yOffset: Double) extends WindowEvent + + /** + * Window close request event. + */ + case object Close extends WindowEvent +} \ No newline at end of file diff --git a/src/main/scala/io/computenode/cyfra/window/WindowSystem.scala b/src/main/scala/io/computenode/cyfra/window/WindowSystem.scala new file mode 100644 index 00000000..d7a8dd59 --- /dev/null +++ b/src/main/scala/io/computenode/cyfra/window/WindowSystem.scala @@ -0,0 +1,31 @@ +package io.computenode.cyfra.window + +/** + * Platform-agnostic interface for window management operations. + */ +trait WindowSystem { + /** + * Creates a new window with specified dimensions and title. + * + * @param width The width of the window in pixels + * @param height The height of the window in pixels + * @param title The window title + * @return A handle to the created window + */ + def createWindow(width: Int, height: Int, title: String): WindowHandle + + /** + * Polls for and returns pending window events. + * + * @return List of window events that occurred since the last poll + */ + def pollEvents(): List[WindowEvent] + + /** + * Checks if a window should close. + * + * @param window The window handle to check + * @return true if the window should close, false otherwise + */ + def shouldWindowClose(window: WindowHandle): Boolean +} \ No newline at end of file diff --git a/src/main/scala/io/computenode/cyfra/window/WindowSystemTest.scala b/src/main/scala/io/computenode/cyfra/window/WindowSystemTest.scala new file mode 100644 index 00000000..c12f0863 --- /dev/null +++ b/src/main/scala/io/computenode/cyfra/window/WindowSystemTest.scala @@ -0,0 +1,102 @@ +package io.computenode.cyfra.window + +import scala.util.{Try, Success, Failure} +import org.lwjgl.glfw.GLFW +import org.lwjgl.system.MemoryUtil + +/** + * Test program for validating the GLFWWindowSystem implementation. + */ +object WindowSystemTest { + // Window dimensions and title + private val Width = 800 + private val Height = 600 + private val Title = "GLFW Window System Test" + + // Target FPS for the main loop + private val TargetFPS = 60 + private val FrameTime = 1.0 / TargetFPS + + def main(args: Array[String]): Unit = { + println("Starting Window System Test") + + // Create window system in a try block to handle initialization errors + try { + val windowSystem = new GLFWWindowSystem() + var window: WindowHandle = null + + try { + // Create a window + println(s"Creating window (${Width}x${Height}: $Title)") + window = windowSystem.createWindow(Width, Height, Title) + + // Enter the main event loop + mainLoop(windowSystem, window) + } catch { + case ex: Exception => + println(s"Error during window operation: ${ex.getMessage}") + ex.printStackTrace() + } finally { + // Clean up window if it was created + if (window != null && window.nativePtr != MemoryUtil.NULL) { + println("Destroying window") + GLFW.glfwDestroyWindow(window.nativePtr) + } + + // Additional cleanup if needed + println("Terminating GLFW") + GLFW.glfwTerminate() + } + } catch { + case ex: Exception => + println(s"Error initializing window system: ${ex.getMessage}") + ex.printStackTrace() + } + + println("Window System Test completed") + } + + /** + * Main application loop that polls and handles events. + */ + private def mainLoop(windowSystem: GLFWWindowSystem, window: WindowHandle): Unit = { + println("Entering main loop") + + var lastTime = GLFW.glfwGetTime() + var frameCount = 0 + var frameTimer = 0.0 + + // Run until the window should close + while (!windowSystem.shouldWindowClose(window)) { + val currentTime = GLFW.glfwGetTime() + val deltaTime = currentTime - lastTime + lastTime = currentTime + + // Update FPS counter + frameCount += 1 + frameTimer += deltaTime + if (frameTimer >= 1.0) { + println(s"FPS: $frameCount") + frameCount = 0 + frameTimer = 0.0 + } + + // Poll and handle window events + val events = windowSystem.pollEvents() + if (events.nonEmpty) { + println(s"Received ${events.size} events:") + windowSystem.handleEvents(events, window) + } + + // Simulate rendering (just sleep to maintain frame rate) + val frameTimeRemaining = FrameTime - (GLFW.glfwGetTime() - currentTime) + if (frameTimeRemaining > 0) { + Thread.sleep((frameTimeRemaining * 1000).toLong) + } + } + + println("Window closed, exiting main loop") + } +} + +// Test this file using - sbt "runMain io.computenode.cyfra.window.WindowSystemTest" \ No newline at end of file