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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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")
Expand Down
43 changes: 43 additions & 0 deletions src/main/scala/io/computenode/cyfra/window/GLFWSystem.scala
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
217 changes: 217 additions & 0 deletions src/main/scala/io/computenode/cyfra/window/GLFWWindowSystem.scala
Original file line number Diff line number Diff line change
@@ -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
}
}
}
84 changes: 84 additions & 0 deletions src/main/scala/io/computenode/cyfra/window/WindowHandle.scala
Original file line number Diff line number Diff line change
@@ -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
}
Loading