Skip to content

Commit

Permalink
Implement a swapchain utility.
Browse files Browse the repository at this point in the history
  • Loading branch information
io7m committed Dec 12, 2024
1 parent 4e37b71 commit 19795a5
Show file tree
Hide file tree
Showing 44 changed files with 4,721 additions and 154 deletions.
91 changes: 91 additions & 0 deletions README.in
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,94 @@ $ mvn clean package
```

If this step fails, it's a bug. Please report it!

## Utilities

### com.io7m.jcoronado.utility.swapchain

The `com.io7m.jcoronado.utility.swapchain` module provides a utility for
managing the [swapchain](https://registry.khronos.org/vulkan/specs/latest/man/html/VK_KHR_swapchain.html)
correctly.

Swapchain management is notoriously difficult, with many pitfalls and sharp
edges. The `JCSwapchainManager` class provides a class for correctly creating
swapchains, automatically recreating them if they become suboptimal or
out-of-date, and acquiring and presenting images.

The class requires the use of the
[VK_EXT_swapchain_maintenance1](https://registry.khronos.org/vulkan/specs/latest/man/html/VK_EXT_swapchain_maintenance1.html)
extension to fix serious design flaws in the original `VK_KHR_swapchain` API.

Briefly, create a swapchain:

```
final var swapChainManager =
resources.add(
JCSwapchainManager.create(
JCSwapchainConfiguration.builder()
.setDevice(device)
.setGraphicsQueue(graphicsQueue)
.setPresentationQueue(presentationQueue)
.setSurface(surface)
.setSurfaceExtension(khrSurfaceExt)
.setSwapChainExtension(khrSwapchainExt)
.addSurfaceAlphaFlags(VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR)
.addImageUsageFlags(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT)
.addImageUsageFlags(VK_IMAGE_USAGE_TRANSFER_DST_BIT)
.addPreferredModes(VK_PRESENT_MODE_MAILBOX_KHR)
.addPreferredModes(VK_PRESENT_MODE_FIFO_KHR)
.addPreferredModes(VK_PRESENT_MODE_IMMEDIATE_KHR)
.build()
)
);
```

Acquire images in the rendering loop:

```
while (rendering) {
try (var image = swapChainManager.acquire()) {
render(image);
image.present();
}
}
```

When an image is acquired or presented, the current swapchain may be detected
as being suboptimal or out-of-date. When this happens, a new swapchain is
created internally and the old one is (eventually) deleted.

On many operating systems, dragging a window's resize box can result in
a flurry of updates that will result in hundreds of swapchain instances being
created and deleted. For best results, disable rendering during window resize
events. As an example, when using [GLFW](https://www.glfw.org/):

```
AtomicBoolean windowIsResizing;

GLFW.glfwSetWindowSizeCallback(
window,
GLFWWindowSizeCallback.create((_, _, _) -> {
windowIsResizing.set(true);
})
);

while (rendering) {
this.windowIsResizing.set(false);
GLFW.glfwPollEvents();

if (!windowIsResizing.get()) {
try (var image = swapChainManager.acquire()) {
render(image);
image.present();
}
} else {
pauseOneFrame();
}
}
```

By avoiding rendering during window resizes, we effectively avoid creating
and destroying swapchains for the intermediate window sizes. When the window
eventually stops resizing, we'll automatically create a suitable swapchain
for the final size.
91 changes: 91 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,94 @@ $ mvn clean package

If this step fails, it's a bug. Please report it!

## Utilities

### com.io7m.jcoronado.utility.swapchain

The `com.io7m.jcoronado.utility.swapchain` module provides a utility for
managing the [swapchain](https://registry.khronos.org/vulkan/specs/latest/man/html/VK_KHR_swapchain.html)
correctly.

Swapchain management is notoriously difficult, with many pitfalls and sharp
edges. The `JCSwapchainManager` class provides a class for correctly creating
swapchains, automatically recreating them if they become suboptimal or
out-of-date, and acquiring and presenting images.

The class requires the use of the
[VK_EXT_swapchain_maintenance1](https://registry.khronos.org/vulkan/specs/latest/man/html/VK_EXT_swapchain_maintenance1.html)
extension to fix serious design flaws in the original `VK_KHR_swapchain` API.

Briefly, create a swapchain:

```
final var swapChainManager =
resources.add(
JCSwapchainManager.create(
JCSwapchainConfiguration.builder()
.setDevice(device)
.setGraphicsQueue(graphicsQueue)
.setPresentationQueue(presentationQueue)
.setSurface(surface)
.setSurfaceExtension(khrSurfaceExt)
.setSwapChainExtension(khrSwapchainExt)
.addSurfaceAlphaFlags(VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR)
.addImageUsageFlags(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT)
.addImageUsageFlags(VK_IMAGE_USAGE_TRANSFER_DST_BIT)
.addPreferredModes(VK_PRESENT_MODE_MAILBOX_KHR)
.addPreferredModes(VK_PRESENT_MODE_FIFO_KHR)
.addPreferredModes(VK_PRESENT_MODE_IMMEDIATE_KHR)
.build()
)
);
```

Acquire images in the rendering loop:

```
while (rendering) {
try (var image = swapChainManager.acquire()) {
render(image);
image.present();
}
}
```

When an image is acquired or presented, the current swapchain may be detected
as being suboptimal or out-of-date. When this happens, a new swapchain is
created internally and the old one is (eventually) deleted.

On many operating systems, dragging a window's resize box can result in
a flurry of updates that will result in hundreds of swapchain instances being
created and deleted. For best results, disable rendering during window resize
events. As an example, when using [GLFW](https://www.glfw.org/):

```
AtomicBoolean windowIsResizing;
GLFW.glfwSetWindowSizeCallback(
window,
GLFWWindowSizeCallback.create((_, _, _) -> {
windowIsResizing.set(true);
})
);
while (rendering) {
this.windowIsResizing.set(false);
GLFW.glfwPollEvents();
if (!windowIsResizing.get()) {
try (var image = swapChainManager.acquire()) {
render(image);
image.present();
}
} else {
pauseOneFrame();
}
}
```

By avoiding rendering during window resizes, we effectively avoid creating
and destroying swapchains for the intermediate window sizes. When the window
eventually stops resizing, we'll automatically create a suitable swapchain
for the final size.

7 changes: 7 additions & 0 deletions checkstyle-filter.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,11 @@
<suppress files="VulkanLWJGLExtDebugUtils.java"
checks="CyclomaticComplexity"/>

<suppress files=".*\.java"
checks="RedundantModifier"/>

<!-- Tracking different failures in submissions. -->
<suppress files="JCSwapchainManager\.java"
checks="ThrowsCount"/>

</suppressions>
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.io7m.jcoronado.api;

import java.util.Map;
import java.util.Objects;
import java.util.Optional;

/**
Expand All @@ -29,22 +30,39 @@ public final class VulkanCallFailedException
/**
* Construct an exception.
*
* @param inErrorCode The returned error code
* @param inFunction The function that failed
* @param message The error message
* @param message The message
* @param cause The cause
* @param attributes The error attributes
*/

public VulkanCallFailedException(
final int inErrorCode,
final String inFunction,
final String message)
final String message,
final Throwable cause,
final Map<String, String> attributes)
{
super(
message,
Map.ofEntries(
Map.entry("ErrorCode", Integer.toString(inErrorCode)),
Map.entry("Function", inFunction)
),
Objects.requireNonNull(message, "message"),
Objects.requireNonNull(cause, "cause"),
Map.copyOf(attributes),
"error-vulkan-call",
Optional.empty()
);
}

/**
* Construct an exception.
*
* @param message The message
* @param attributes The error attributes
*/

public VulkanCallFailedException(
final String message,
final Map<String, String> attributes)
{
super(
Objects.requireNonNull(message, "message"),
Map.copyOf(attributes),
"error-vulkan-call",
Optional.empty()
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.io7m.jcoronado.api;

import java.util.HashMap;
import java.util.Objects;

/**
Expand Down Expand Up @@ -63,20 +64,18 @@ public static VulkanCallFailedException failed(
final int code,
final String function)
{
return new VulkanCallFailedException(
code,
function,
new StringBuilder(64)
.append("Function ")
.append(function)
.append(" returned 0x")
.append(Integer.toUnsignedString(code, 16))
.append(" (")
.append(code)
.append(") (")
.append(VulkanErrorCodes.errorName(code).orElse(
"Unrecognized error code"))
.append(')')
.toString());
final var errorCode =
"0x" + Integer.toUnsignedString(code, 16);

final var errorName =
VulkanErrorCodes.errorName(code)
.orElse("Unrecognized error code");

final var attributes = new HashMap<String, String>(3);
attributes.put("Function", function);
attributes.put("ErrorCode", errorCode);
attributes.put("ErrorName", errorName);

return new VulkanCallFailedException("Vulkan call failed.", attributes);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package com.io7m.jcoronado.api;

import java.util.List;

/**
* An extension.
*/
Expand All @@ -27,4 +29,13 @@ public interface VulkanExtensionType
*/

String name();

/**
* @return The extra extension names that are also required (such as "VK_EXT_swapchain_maintenance1")
*/

default List<String> extraNames()
{
return List.of();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright © 2018 Mark Raynsford <[email protected]> http://io7m.com
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
* IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/

package com.io7m.jcoronado.api;

import java.util.Map;
import java.util.Objects;
import java.util.Optional;

/**
* An exception raised by a problem with the Vulkan implementation (such as
* violating the Vulkan specification).
*/

public final class VulkanImplementationException
extends VulkanException
{
/**
* Construct an exception.
*
* @param message The message
* @param cause The cause
* @param attributes The error attributes
*/

public VulkanImplementationException(
final String message,
final Throwable cause,
final Map<String, String> attributes)
{
super(
Objects.requireNonNull(message, "message"),
Objects.requireNonNull(cause, "cause"),
Map.copyOf(attributes),
"error-vulkan-call",
Optional.empty()
);
}

/**
* Construct an exception.
*
* @param message The message
* @param attributes The error attributes
*/

public VulkanImplementationException(
final String message,
final Map<String, String> attributes)
{
super(
Objects.requireNonNull(message, "message"),
Map.copyOf(attributes),
"error-vulkan-implementation-incorrect",
Optional.empty()
);
}
}
Loading

0 comments on commit 19795a5

Please sign in to comment.