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
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.computenode.cyfra.rtrp

import org.lwjgl.vulkan.VkExtent2D
import org.lwjgl.vulkan.{VkDevice, VkExtent2D}
import org.lwjgl.vulkan.KHRSwapchain.vkDestroySwapchainKHR
import org.lwjgl.vulkan.VK10.vkDestroyImageView
import io.computenode.cyfra.vulkan.util.VulkanObjectHandle

class Swapchain(
val device: VkDevice,
override val handle: Long,
val images: Array[Long],
val imageViews: Array[Long],
val format: Int,
val colorSpace: Int,
val extent: VkExtent2D,
) extends VulkanObjectHandle:

override protected def close(): Unit =
if imageViews != null then
imageViews.foreach: imageView =>
if imageView != 0L then vkDestroyImageView(device, imageView, null)

vkDestroySwapchainKHR(device, handle, null)
alive = false
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package io.computenode.cyfra.rtrp

import io.computenode.cyfra.vulkan.VulkanContext
import io.computenode.cyfra.vulkan.util.Util.{check, pushStack}
import io.computenode.cyfra.rtrp.surface.core.*
import io.computenode.cyfra.rtrp.surface.vulkan.*
import org.lwjgl.system.MemoryStack
import org.lwjgl.vulkan.KHRSurface.*
import org.lwjgl.vulkan.KHRSwapchain.*
import org.lwjgl.vulkan.VK10.*
import io.computenode.cyfra.vulkan.util.{VulkanAssertionError, VulkanObjectHandle}
import org.lwjgl.vulkan.{
VkExtent2D,
VkSwapchainCreateInfoKHR,
VkImageViewCreateInfo,
VkSurfaceFormatKHR,
VkPresentInfoKHR,
VkSemaphoreCreateInfo,
VkSurfaceCapabilitiesKHR,
}
import scala.util.{Try, Success, Failure}

import scala.collection.mutable.ArrayBuffer

private[cyfra] class SwapchainManager(context: VulkanContext, surface: Surface):

private val device = context.device
private val physicalDevice = device.physicalDevice
private var swapchainHandle: Long = VK_NULL_HANDLE
private var swapchainImages: Array[Long] = _

private var swapchainImageFormat: Int = _
private var swapchainColorSpace: Int = _
private var swapchainPresentMode: Int = _
private var swapchainExtent: VkExtent2D = _
private var swapchainImageViews: Array[Long] = _

// Get the raw Vulkan capabilities for low-level access
private val vkCapabilities = pushStack: Stack =>
val vkCapabilities = VkSurfaceCapabilitiesKHR.calloc(Stack)
check(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physicalDevice, surface.nativeHandle, vkCapabilities), "Failed to get surface capabilities")
vkCapabilities

// Get the high-level surface capabilities for format/mode queries
private val surfaceCapabilities = surface.getCapabilities() match
case Success(caps) => caps
case Failure(exception) =>
throw new RuntimeException("Failed to get surface capabilities", exception)

val (width, height) = (vkCapabilities.currentExtent().width(), vkCapabilities.currentExtent().height())
val minImageExtent = vkCapabilities.minImageExtent()
val maxImageExtent = vkCapabilities.maxImageExtent()

def initialize(surfaceConfig: SurfaceConfig): Swapchain = pushStack: Stack =>
// cleanup()

val preferredPresentMode = surfaceConfig.preferredPresentMode

// Use the surface capabilities abstraction
val availableFormats: List[Int] = surfaceCapabilities.supportedFormats
val availableColorSpaces: List[Int] = surfaceCapabilities.supportedColorSpaces

val exactFmtMatch = availableFormats.find(fmt => fmt == surfaceConfig.preferredFormat)
val exactCsMatch = availableColorSpaces.find(cs => cs == surfaceConfig.preferredColorSpace)

val chosenFormat = exactFmtMatch.orElse(availableFormats.headOption).getOrElse(throw new RuntimeException("No supported surface formats available"))
val chosenColorSpace =
exactCsMatch.orElse(availableColorSpaces.headOption).getOrElse(throw new RuntimeException("No supported color spaces available"))

// Choose present mode
val availableModes = surfaceCapabilities.supportedPresentModes
val presentMode = if availableModes.contains(preferredPresentMode) then preferredPresentMode else VK_PRESENT_MODE_FIFO_KHR

// Choose swap extent
val (chosenWidth, chosenHeight) =
if width != -1 && height != -1 then (width, height)
else
val (desiredWidth, desiredHeight) = (800, 600) // TODO: get from window/config
if surfaceCapabilities.isExtentSupported(desiredWidth, desiredHeight) then (desiredWidth, desiredHeight)
else surfaceCapabilities.clampExtent(desiredWidth, desiredHeight)

// Determine image count
var imageCount = surfaceCapabilities.minImageCount + 1
if surfaceCapabilities.maxImageCount != 0 then imageCount = Math.min(imageCount, surfaceCapabilities.maxImageCount)

// Convert from surface abstraction to Vulkan constants
swapchainImageFormat = chosenFormat
swapchainColorSpace = chosenColorSpace
swapchainPresentMode = presentMode
swapchainExtent = VkExtent2D.calloc(Stack).width(chosenWidth).height(chosenHeight)

// Create swapchain
val createInfo = VkSwapchainCreateInfoKHR
.calloc(Stack)
.sType$Default()
.surface(surface.nativeHandle)
.minImageCount(imageCount)
.imageFormat(swapchainImageFormat)
.imageColorSpace(swapchainColorSpace)
.imageExtent(swapchainExtent)
.imageArrayLayers(1)
.imageUsage(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT)
.preTransform(vkCapabilities.currentTransform())
.compositeAlpha(VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR)
.presentMode(swapchainPresentMode)
.clipped(true)
.oldSwapchain(VK_NULL_HANDLE)
.imageSharingMode(VK_SHARING_MODE_EXCLUSIVE)
.queueFamilyIndexCount(0)
.pQueueFamilyIndices(null)

val pSwapchain = Stack.callocLong(1)

val result = vkCreateSwapchainKHR(device.get, createInfo, null, pSwapchain)
if result != VK_SUCCESS then throw new VulkanAssertionError("Failed to create swap chain", result)

swapchainHandle = pSwapchain.get(0)

// Get swap chain images
val pImageCount = Stack.callocInt(1)
vkGetSwapchainImagesKHR(device.get, swapchainHandle, pImageCount, null)
val actualImageCount = pImageCount.get(0)

val pSwapchainImages = Stack.callocLong(actualImageCount)
vkGetSwapchainImagesKHR(device.get, swapchainHandle, pImageCount, pSwapchainImages)

swapchainImages = new Array[Long](actualImageCount)
for i <- 0 until actualImageCount do swapchainImages(i) = pSwapchainImages.get(i)

createImageViews()

Swapchain(
device = device.get,
handle = swapchainHandle,
images = swapchainImages,
imageViews = swapchainImageViews,
format = swapchainImageFormat,
colorSpace = swapchainColorSpace,
extent = swapchainExtent,
)

private def createImageViews(): Unit = pushStack: Stack =>
if swapchainImages == null || swapchainImages.isEmpty then
throw new VulkanAssertionError("Cannot create image views: swap chain images not initialized", -1)

if swapchainImageViews != null then
swapchainImageViews.foreach(imageView => if imageView != VK_NULL_HANDLE then vkDestroyImageView(device.get, imageView, null))

swapchainImageViews = new Array[Long](swapchainImages.length)

for i <- swapchainImages.indices do
val createInfo = VkImageViewCreateInfo
.calloc(Stack)
.sType$Default()
.image(swapchainImages(i))
.viewType(VK_IMAGE_VIEW_TYPE_2D)
.format(swapchainImageFormat)

createInfo.components: components =>
components
.r(VK_COMPONENT_SWIZZLE_IDENTITY)
.g(VK_COMPONENT_SWIZZLE_IDENTITY)
.b(VK_COMPONENT_SWIZZLE_IDENTITY)
.a(VK_COMPONENT_SWIZZLE_IDENTITY)

createInfo.subresourceRange: range =>
range
.aspectMask(VK_IMAGE_ASPECT_COLOR_BIT)
.baseMipLevel(0)
.levelCount(1)
.baseArrayLayer(0)
.layerCount(1)

val pImageView = Stack.callocLong(1)
check(vkCreateImageView(device.get, createInfo, null, pImageView), s"Failed to create image view for swap chain image $i")
swapchainImageViews(i) = pImageView.get(i)
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ object SurfaceIntegrationExample:

private def createTestWindows(
manager: WindowManager,
): List[(io.computenode.cyfra.rtrp.window.core.Window, io.computenode.cyfra.rtrp.surface.core.RenderSurface)] =
): List[(io.computenode.cyfra.rtrp.window.core.Window, io.computenode.cyfra.rtrp.surface.core.Surface)] =
val configs = List(
// Main window - gaming configuration
(WindowConfig(width = 1024, height = 768, title = "Main Window", position = Some(WindowPosition.Centered)), SurfaceConfig.gaming),
Expand All @@ -104,7 +104,7 @@ object SurfaceIntegrationExample:
List.empty

private def inspectSurfaceCapabilities(
windowSurfacePairs: List[(io.computenode.cyfra.rtrp.window.core.Window, io.computenode.cyfra.rtrp.surface.core.RenderSurface)],
windowSurfacePairs: List[(io.computenode.cyfra.rtrp.window.core.Window, io.computenode.cyfra.rtrp.surface.core.Surface)],
): Unit =
windowSurfacePairs.foreach { case (window, surface) =>
println(s"\n Surface ${surface.id} (Window: ${window.properties.title}):")
Expand All @@ -127,7 +127,7 @@ object SurfaceIntegrationExample:

private def runMainLoop(
manager: WindowManager,
windowSurfacePairs: List[(io.computenode.cyfra.rtrp.window.core.Window, io.computenode.cyfra.rtrp.surface.core.RenderSurface)],
windowSurfacePairs: List[(io.computenode.cyfra.rtrp.window.core.Window, io.computenode.cyfra.rtrp.surface.core.Surface)],
): Unit =
var frameCount = 0
val maxFrames = 300 // 5 seconds at 60fps
Expand Down Expand Up @@ -159,7 +159,7 @@ object SurfaceIntegrationExample:

logger.info("Main loop completed")

private def testSurfaceRecreation(manager: WindowManager, surface: io.computenode.cyfra.rtrp.surface.core.RenderSurface): Unit =
private def testSurfaceRecreation(manager: WindowManager, surface: io.computenode.cyfra.rtrp.surface.core.Surface): Unit =
logger.info(s"Testing recreation of surface ${surface.id}...")

manager.getSurfaceManager() match
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ import io.computenode.cyfra.utility.Logger.logger
class SurfaceManager(vulkanContext: VulkanContext):

private val surfaceFactory = new VulkanSurfaceFactory(vulkanContext)
private val activeSurfaces = mutable.Map[WindowId, RenderSurface]()
private val activeSurfaces = mutable.Map[WindowId, Surface]()
private val surfaceConfigs = mutable.Map[WindowId, SurfaceConfig]()
private val eventHandlers = mutable.Map[Class[? <: SurfaceEvent], SurfaceEvent => Unit]()

// Create a surface for a window.
def createSurface(window: Window, config: SurfaceConfig = SurfaceConfig.default): Try[RenderSurface] =
def createSurface(window: Window, config: SurfaceConfig = SurfaceConfig.default): Try[Surface] =
if activeSurfaces.contains(window.id) then return Failure(new IllegalStateException(s"Surface already exists for window ${window.id}"))

surfaceFactory
Expand All @@ -33,7 +33,7 @@ class SurfaceManager(vulkanContext: VulkanContext):

surface

def createSurfaces(windows: List[Window], config: SurfaceConfig = SurfaceConfig.default): Try[List[RenderSurface]] =
def createSurfaces(windows: List[Window], config: SurfaceConfig = SurfaceConfig.default): Try[List[Surface]] =
val results = windows.map(createSurface(_, config))

val failures = results.collect { case Failure(ex) => ex }
Expand All @@ -42,10 +42,10 @@ class SurfaceManager(vulkanContext: VulkanContext):
Failure(new RuntimeException(s"Failed to create ${failures.size} surfaces"))
else Success(results.collect { case Success(surface) => surface })

def getSurface(windowId: WindowId): Option[RenderSurface] =
def getSurface(windowId: WindowId): Option[Surface] =
activeSurfaces.get(windowId)

def getActiveSurfaces(): Map[WindowId, RenderSurface] = activeSurfaces.toMap
def getActiveSurfaces(): Map[WindowId, Surface] = activeSurfaces.toMap

def getSurfaceConfig(windowId: WindowId): Option[SurfaceConfig] =
surfaceConfigs.get(windowId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ package io.computenode.cyfra.rtrp.surface.core
import io.computenode.cyfra.rtrp.window.core.*
import scala.util.Try

// Unique id for surfaces
case class SurfaceId(value: Long) extends AnyVal

// Render surface abstraction
trait RenderSurface:
trait Surface:
def id: SurfaceId
def windowId: WindowId
def nativeHandle: Long
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ package io.computenode.cyfra.rtrp.surface.core

// Surface capabilities - what the surface can do
trait SurfaceCapabilities:
def supportedFormats: List[SurfaceFormat]
def supportedColorSpaces: List[ColorSpace]
def supportedPresentModes: List[PresentMode]
def supportedFormats: List[Int]
def supportedColorSpaces: List[Int]
def supportedPresentModes: List[Int]
def minImageExtent: (Int, Int)
def maxImageExtent: (Int, Int)
def currentExtent: (Int, Int)
Expand All @@ -13,16 +13,16 @@ trait SurfaceCapabilities:
def supportsAlpha: Boolean
def supportsTransform: Boolean

def supportsFormat(format: SurfaceFormat): Boolean =
def supportsFormat(format: Int): Boolean =
supportedFormats.contains(format)

def supportsPresentMode(mode: PresentMode): Boolean =
def supportsPresentMode(mode: Int): Boolean =
supportedPresentModes.contains(mode)

def chooseBestFormat(preferences: List[SurfaceFormat]): Option[SurfaceFormat] =
def chooseBestFormat(preferences: List[Int]): Option[Int] =
preferences.find(supportsFormat)

def chooseBestPresentMode(preferences: List[PresentMode]): Option[PresentMode] =
def chooseBestPresentMode(preferences: List[Int]): Option[Int] =
preferences.find(supportsPresentMode)

// Check if the given extent is within supported bounds
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
package io.computenode.cyfra.rtrp.surface.core

import org.lwjgl.vulkan.VK10.*
import org.lwjgl.vulkan.KHRSurface.*
import org.lwjgl.vulkan.KHRSwapchain.*
// Configuration for surface creation
case class SurfaceConfig(
preferredFormat: SurfaceFormat = SurfaceFormat.B8G8R8A8_SRGB,
preferredColorSpace: ColorSpace = ColorSpace.SRGB_NONLINEAR,
preferredPresentMode: PresentMode = PresentMode.MAILBOX,
preferredFormat: Int = VK_FORMAT_B8G8R8A8_SRGB,
preferredColorSpace: Int = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR,
preferredPresentMode: Int = VK_PRESENT_MODE_MAILBOX_KHR,
enableVSync: Boolean = true,
minImageCount: Option[Int] = None,
maxImageCount: Option[Int] = None,
):

// Create a copy with different format, present mode, VSync settings, or image count constraints
def withFormat(format: SurfaceFormat): SurfaceConfig =
def withFormat(format: Int): SurfaceConfig =
copy(preferredFormat = format)

def withPresentMode(mode: PresentMode): SurfaceConfig =
def withPresentMode(mode: Int): SurfaceConfig =
copy(preferredPresentMode = mode)

def withVSync(enabled: Boolean): SurfaceConfig =
val mode = if enabled then PresentMode.FIFO else PresentMode.IMMEDIATE
val mode = if enabled then VK_PRESENT_MODE_FIFO_KHR else VK_PRESENT_MODE_IMMEDIATE_KHR
copy(enableVSync = enabled, preferredPresentMode = mode)

def withImageCount(min: Int, max: Int): SurfaceConfig =
Expand All @@ -30,23 +33,23 @@ object SurfaceConfig:
def default: SurfaceConfig = SurfaceConfig()

def gaming: SurfaceConfig = SurfaceConfig(
preferredFormat = SurfaceFormat.B8G8R8A8_SRGB,
preferredPresentMode = PresentMode.MAILBOX,
preferredFormat = VK_FORMAT_B8G8R8A8_SRGB,
preferredPresentMode = VK_PRESENT_MODE_MAILBOX_KHR,
enableVSync = false,
minImageCount = Some(2),
maxImageCount = Some(3),
)

def quality: SurfaceConfig = SurfaceConfig(
preferredFormat = SurfaceFormat.R8G8B8A8_SRGB,
preferredColorSpace = ColorSpace.DISPLAY_P3_NONLINEAR,
preferredPresentMode = PresentMode.FIFO,
preferredFormat = VK_FORMAT_R8G8B8A8_SRGB,
preferredColorSpace = 1000104001,
preferredPresentMode = VK_PRESENT_MODE_FIFO_KHR,
enableVSync = true,
)

def lowLatency: SurfaceConfig = SurfaceConfig(
preferredFormat = SurfaceFormat.B8G8R8A8_UNORM,
preferredPresentMode = PresentMode.IMMEDIATE,
preferredFormat = VK_FORMAT_B8G8R8A8_UNORM,
preferredPresentMode = VK_PRESENT_MODE_IMMEDIATE_KHR,
enableVSync = false,
minImageCount = Some(1),
maxImageCount = Some(2),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ object SurfaceEvent:

case class SurfaceLost(windowId: WindowId, surfaceId: SurfaceId, error: String) extends SurfaceEvent

case class SurfaceFormatChanged(windowId: WindowId, surfaceId: SurfaceId, oldFormat: SurfaceFormat, newFormat: SurfaceFormat) extends SurfaceEvent
case class FormatChanged(windowId: WindowId, surfaceId: SurfaceId, oldFormat: Int, newFormat: Int) extends SurfaceEvent
Loading