Skip to content

Conversation

@rudrabeniwal
Copy link
Collaborator

Key Abstractions and Component Relationships

Core Abstractions

  1. RenderSurface: The primary abstraction representing a surface that can be rendered to

    • It provides operations for sizing, querying capabilities, and lifecycle management
    • Associates surfaces with window IDs and maintains validity state
  2. SurfaceCapabilities: Defines what a surface can do

    • Exposes supported formats, color spaces, presentation modes
    • Provides size constraints and image count limits
    • Includes helper methods for choosing optimal configurations
  3. SurfaceConfig: Configuration parameters for surface creation

    • Specifies preferred format, color space, and presentation mode
    • Controls VSync and image count settings
    • Provides predefined configurations (gaming, quality, low-latency)
  4. SurfaceEvents: Surface-specific events

    • Created, destroyed, recreated, lost events
    • Capabilities and format change notifications

Vulkan Implementations

  1. VulkanSurface: Vulkan implementation of RenderSurface

    • Manages the underlying Vulkan KHR surface handle
    • Handles surface state, validity, and recreation
  2. VulkanSurfaceCapabilities: Queries and maps Vulkan-specific capabilities

    • Translates Vulkan formats, color spaces, and presentation modes to abstract types
    • Queries and caches physical device surface capabilities
  3. VulkanSurfaceFactory: Creates Vulkan surfaces from window handles

    • Bridges GLFW window system to Vulkan surfaces
    • Validates surface configurations against device capabilities

Central Coordination

SurfaceManager is the central coordinator that:

  • Creates and destroys surfaces
  • Manages surface lifecycles and configurations
  • Handles window events and propagates surface events
  • Provides surface statistics and status tracking

Public API and Main Entry Points

Creating and Managing Surfaces

// Initialize with Vulkan context
val surfaceManager = new SurfaceManager(vulkanContext)

// Create a surface for a window
val surface: Try[RenderSurface] = 
  surfaceManager.createSurface(window, SurfaceConfig.gaming)

// Get an existing surface
val maybeSurface: Option[RenderSurface] = 
  surfaceManager.getSurface(windowId)

// Destroy a surface when no longer needed
surfaceManager.destroySurface(windowId)

Handling Surface Events

// Register for surface creation events
surfaceManager.onSurfaceCreated { event =>
  println(s"Surface ${event.surfaceId} created for window ${event.windowId}")
}

// Register for surface destruction events
surfaceManager.onSurfaceDestroyed { event =>
  println(s"Surface ${event.surfaceId} destroyed")
}

// Handle surface loss (device lost scenarios)
surfaceManager.onSurfaceLost { event =>
  println(s"Surface lost: ${event.error}")
  surfaceManager.recreateSurface(event.windowId)
}

Integration with WindowManager

The WindowManager provides convenient methods for creating windows with surfaces:

// Create a window with a surface
val result: Try[(Window, RenderSurface)] = 
  windowManager.createWindowWithSurface(windowConfig, surfaceConfig)

// Create multiple window-surface pairs
val configs = List(
  (WindowConfig(...), SurfaceConfig.gaming),
  (WindowConfig(...), SurfaceConfig.quality)
)
val pairs: Try[List[(Window, RenderSurface)]] = 
  windowManager.createWindowsWithSurfaces(configs)

Complete Integration Example

The SurfaceIntegrationExample demonstrates a complete integration workflow, including:

  • Window and surface creation
  • Event handler registration
  • Surface capability inspection
  • Main rendering loop
  • Surface recreation testing

Integration of the Surface module in our application

1. Setup and Initialization with Vulkan Context

  • 1.1. Initialize GLFW (GLFW.glfwInit())
  • 1.2. Create Vulkan Context with Surface Support (VulkanContext.withSurfaceSupport())
  • 1.3. Initialize WindowManager with Vulkan Support (Using higher-order function style like WindowManager.withVulkanManager(vulkanContext) { manager => ... } or Manual creation and cleanup like:
val manager = WindowManager.createWithVulkan(vulkanContext)
try {
 // Use the manager here
} finally {
 manager.shutdown()
}

2. Window-Surface Creation Patterns

  • 2.1. Creating a Single Window with Surface
  • 2.2. Creating Multiple Windows with Surfaces
  • 2.3. Advanced Configuration (custom surface configurations for specific rendering needs)
    for example:
// Create a custom configuration
val customConfig = SurfaceConfig()
  .withFormat(SurfaceFormat.R8G8B8A8_SRGB)
  .withPresentMode(PresentMode.MAILBOX)
  .withVSync(false)
  .withImageCount(2, 3)

// Apply to window creation
manager.createWindowWithSurface(
  WindowConfig(width = 1280, height = 720, title = "Custom Config"),
  customConfig
)

3. Event Handling Integration

  • 3.1. Setting Up Window Event Handlers (like manager.onWindowResize { event => ... })
  • 3.2. Setting Up Surface Event Handlers (like manager.onSurfaceCreated { event => ... })
  • 3.3. Processing Events in the Main Loop

4. Surface Lifecycle Management

  • 4.1. Inspecting Surface Capabilities
  • 4.2. Handling Surface Resizing
  • 4.3. Surface Destruction
  • 4.4. Application Shutdown

5. Error Recovery Strategies

  • 5.1. Detecting Surface Loss
manager.onSurfaceLost { event =>
  println(s"Surface ${event.surfaceId} lost: ${event.error}")
  
  // Implement recovery strategy
  recoverFromSurfaceLoss(event.windowId, event.surfaceId)
}
  • 5.2. Basic Recovery: Surface Recreation
  • 5.3. Advanced Recovery: Tiered Strategy
    • Tier 1: Try simple recreation
    • Tier 2: Try to create a new surface
    • Tier 3: Try full window recreation
  • 5.4. Handling Device Loss (for scenarios like complete device loss)
      1. Save application state (saveApplicationState())
      1. Shut down current Vulkan context (manager.shutdown())
      1. Wait for a moment to allow device recovery (Thread.sleep(1000))
      1. Try to create a new Vulkan context
      1. Create new window manager
      1. Recreate windows and surfaces
      1. Restore application state (restoreApplicationState())

Event Propagation

┌─────────────┐    WindowEvent     ┌───────────────┐                     
│ Window      │ ----------------> │ WindowManager │ ----------------> Application Code
│ System      │                   └───────────────┘    WindowEvent      
└─────────────┘                          |                             
                                         | WindowEvent                  
                                         v                             
                                ┌───────────────┐    SurfaceEvent     
                                │ SurfaceManager│ ----------------> Application Code
                                └───────────────┘                     

Window System Integration

┌────────────────┐ events   ┌─────────────────────┐
│ WindowManager  │ -------> │ SurfaceManager      │
└────────────────┘          └─────────────────────┘
        │                             │
        │ owns                        │ manages
        ▼                             ▼
┌────────────────┐ mapped to ┌─────────────────────┐
│ Window         │ --------> │ RenderSurface       │
└────────────────┘           └─────────────────────┘

@rudrabeniwal rudrabeniwal requested a review from szymon-rd July 9, 2025 15:09
@rudrabeniwal rudrabeniwal changed the base branch from main to cyfra-rtrp July 9, 2025 15:21
object SurfaceIntegrationExample:

def main(args: Array[String]): Unit =
println("=== Cyfra Surface Integration Example ===")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try using the logger instead of printlns - import io.computenode.cyfra.utility.Logger.logger and then you can do logger.debug(...), logger.info(...), logger.warn(..)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is better because it is configurable on the side of the library user - they can write a configuration of where they want the logs to go and how they want them to be formatted. Usually in production apps you will use loggers instead of println.

Comment on lines 77 to 97
activeSurfaces
.get(windowId)
.foreach: surface =>
surface.resize(width, height) match
case Success(_) =>
// Surface resized successfully, capabilities might have changed
surface
.getCapabilities()
.foreach: newCapabilities =>
// Fire capabilities changed event (we'd need to compare with old capabilities)
fireEvent(
SurfaceEvent.SurfaceCapabilitiesChanged(
windowId,
surface.id,
newCapabilities,
newCapabilities, // TODO: track old capabilities
),
)

case Failure(ex) =>
println(s"Warning: Failed to resize surface for window $windowId: ${ex.getMessage}")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be rewritten as a for loop - There are two levels of nested foreach. You will need to use some methods of Try to handle the error.

Comment on lines 77 to 97
activeSurfaces
.get(windowId)
.foreach: surface =>
surface.resize(width, height) match
case Success(_) =>
// Surface resized successfully, capabilities might have changed
surface
.getCapabilities()
.foreach: newCapabilities =>
// Fire capabilities changed event (we'd need to compare with old capabilities)
fireEvent(
SurfaceEvent.SurfaceCapabilitiesChanged(
windowId,
surface.id,
newCapabilities,
newCapabilities, // TODO: track old capabilities
),
)

case Failure(ex) =>
println(s"Warning: Failed to resize surface for window $windowId: ${ex.getMessage}")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be logger.error


case WindowEvent.CloseRequested(windowId) =>
destroySurface(windowId).recover { case ex =>
println(s"Warning: Failed to destroy surface for closing window $windowId: ${ex.getMessage}")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all those fails should be logger.error

@szymon-rd szymon-rd self-requested a review July 29, 2025 08:10
@rudrabeniwal rudrabeniwal marked this pull request as ready for review July 29, 2025 09:13
@rudrabeniwal rudrabeniwal merged commit 20c5844 into cyfra-rtrp Jul 29, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants