diff --git a/docs/initialization.md b/docs/initialization.md
index b731c2a..04032c2 100644
--- a/docs/initialization.md
+++ b/docs/initialization.md
@@ -28,6 +28,8 @@ to use virtual reality (OpenXR), where the OpenXR runtime can set
`XrGraphicsRequirementsVulkanKHR.maxApiVersionSupported` to
`VK_API_VERSION_1_0` during `xrGetVulkanGraphicsRequirementsKHR`,
which will pin you on Vulkan 1.0, even if the device supports 1.3.
+The OpenXR runtime for PCVR for Oculus Quest 2 did this last time
+I checked (21-09-2024).
- As far as I know, targeting Vulkan 1.1 is useless on desktop.
## Instance creation properties
diff --git a/docs/methods.md b/docs/methods.md
index 8fef909..732e23d 100644
--- a/docs/methods.md
+++ b/docs/methods.md
@@ -607,4 +607,90 @@ You can call `session.attach(stack, actionSet)` to attach a
single `XrActionSet` to the `XrSession` (using
`xrAttachSessionActionSets`). If you wish to attach multiple
action sets at the same time, you will have to call
-`xrAttachSessionActionSets` yourself.
\ No newline at end of file
+`xrAttachSessionActionSets` yourself.
+
+### The session loop
+Once all action sets and Vulkan resources are set up, it's time
+to start the session loop. Managing a session loop is a lot of
+work (code) that shouldn't depend much on the application.
+The `SessionLoop` class was made to lighten this work for
+applications. It is an abstract class that handles most of the
+work, but you need to implement several methods:
+
+#### Creating the projection matrix
+You need to implement the method
+`Matrix4f createProjectionMatrix(XrFovf fov)`.
+You could easily do this by returning
+`xr.createProjectionMatrix(fov, nearPlane, farPlane)`,
+but you can also create a more complicated projection matrix.
+
+#### Choosing the active action set
+You need to implement the method
+`XrActionSet[] chooseActiveActionSets()`. If your application
+only has 1 action set, you could attach it before starting
+the session loop, and always return it.
+
+If your application actually switches action sets, it becomes
+a bit more complicated. When you want to change the active
+action set, you will need to *attach* the new action set during
+this method, and then return it. If you want to keep using the
+same action set as the last call, just return that same
+action set(s).
+
+#### The update method
+You need to implement the method `void update()`, which will
+be called during every iteration of the session loop, always
+after polling and handling events. You can do whatever you
+want in this method, and are allowed to leave the method body
+empty.
+
+#### Handling events
+You need to implement the method
+`void handleEvent(XrEventDataBuffer event)`. This method will
+be called for each event that is polled with `xrPollEvent`.
+Just like in the update method, you can do whatever you want
+in this method, including leaving the method body empty.
+
+Note that the `SessionLoop` class will automatically handle
+`XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED` events before calling
+`handleEvent`. You may listen to session state changes, but you
+don't **need** to.
+
+#### Waiting for render resources
+You need to implement the method
+`void waitForRenderResources(MemoryStack stack)`. You should use
+this method to wait for render resources (like command buffers
+and fences) that you will need during the *next* frame.
+
+This method will only be called during session loop iterations
+where the session state is either `SYNCHRONIZED`, `VISIBLE`,
+`FOCUSSED`, or `READY`. Note that this does *not* necessarily
+mean that a frame will be rendered during this iteration.
+
+#### Recording render commands
+You need to implement the method
+```java
+void recordRenderCommands(
+ MemoryStack stack, XrFrameState frameState, int swapchainImageIndex, Matrix4f[] cameraMatrices
+)
+```
+During this method, you should record all the command buffers
+that you intend to submit for this frame. This method will be
+called right after `xrAcquireSwapchainImage` and
+`xrSyncActions`, which means that the next swapchain image is
+known, but that it's not yet ready.
+
+Therefor, you may (and should) record command buffers that use
+the swapchain image, but you may not submit them yet. You may
+submit any command buffers that do **not** need the swapchain
+image.
+
+Since this method is called right after `xrSyncActions`, it is
+also the best moment to query actions.
+
+#### Submitting the render commands
+You need to implement the method `void submitRenderCommands()`.
+During this method, you should submit the command buffers that
+you recorded during `recordRenderCommands`. This method will be
+called right after `xrWaitSwapchainImage` and right before
+`xrReleaseSwapchainImage`.
\ No newline at end of file
diff --git a/samples/src/main/java/com/github/knokko/boiler/samples/HelloXR.java b/samples/src/main/java/com/github/knokko/boiler/samples/HelloXR.java
index b424c56..3997708 100644
--- a/samples/src/main/java/com/github/knokko/boiler/samples/HelloXR.java
+++ b/samples/src/main/java/com/github/knokko/boiler/samples/HelloXR.java
@@ -1,5 +1,6 @@
package com.github.knokko.boiler.samples;
+import com.github.knokko.boiler.buffers.MappedVkbBuffer;
import com.github.knokko.boiler.builders.BoilerBuilder;
import com.github.knokko.boiler.builders.xr.BoilerXrBuilder;
import com.github.knokko.boiler.commands.CommandRecorder;
@@ -28,6 +29,8 @@
public class HelloXR {
+ private static final int NUM_FRAMES_IN_FLIGHT = 2;
+
public static void main(String[] args) throws InterruptedException {
var boiler = new BoilerBuilder(
VK_API_VERSION_1_0, "HelloXR", 1
@@ -94,11 +97,12 @@ public static void main(String[] args) throws InterruptedException {
VK_IMAGE_ASPECT_DEPTH_BIT, VK_SAMPLE_COUNT_1_BIT, 1, 2, true, "DepthImage"
);
- var commandPool = boiler.commands.createPool(
- VK_COMMAND_POOL_CREATE_TRANSIENT_BIT, boiler.queueFamilies().graphics().index(), "Drawing"
+ var commandPools = boiler.commands.createPools(
+ VK_COMMAND_POOL_CREATE_TRANSIENT_BIT, boiler.queueFamilies().graphics().index(),
+ NUM_FRAMES_IN_FLIGHT, "Drawing"
);
- var commandBuffer = boiler.commands.createPrimaryBuffers(commandPool, 1, "Drawing")[0];
- var fence = boiler.sync.fenceBank.borrowFence(false, "Drawing");
+ var commandBuffers = boiler.commands.createPrimaryBufferPerPool("Drawing", commandPools);
+ var fences = boiler.sync.fenceBank.borrowFences(NUM_FRAMES_IN_FLIGHT, false, "Drawing");
int vertexSize = (3 + 3) * 4;
var vertexBuffer = boiler.buffers.createMapped(
@@ -124,13 +128,16 @@ public static void main(String[] args) throws InterruptedException {
hostIndexBuffer.put(1).put(2).put(3); // right of the hand triangle
hostIndexBuffer.put(2).put(0).put(3); // left of the hand triangle
- var matrixBuffer = boiler.buffers.createMapped(
- 5 * 64, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, "MatrixBuffer"
- );
+ var matrixBuffers = new MappedVkbBuffer[NUM_FRAMES_IN_FLIGHT];
+ for (int index = 0; index < NUM_FRAMES_IN_FLIGHT; index++) {
+ matrixBuffers[index] = boiler.buffers.createMapped(
+ 5 * 64, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, "MatrixBuffer"
+ );
+ }
VkbDescriptorSetLayout descriptorSetLayout;
HomogeneousDescriptorPool descriptorPool;
- long descriptorSet;
+ long[] descriptorSets;
long pipelineLayout;
long graphicsPipeline;
try (var stack = stackPush()) {
@@ -139,15 +146,17 @@ public static void main(String[] args) throws InterruptedException {
boiler.descriptors.binding(layoutBindings, 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT);
descriptorSetLayout = boiler.descriptors.createLayout(stack, layoutBindings, "MatricesLayout");
- descriptorPool = descriptorSetLayout.createPool(1, 0, "MatricesPool");
- descriptorSet = descriptorPool.allocate(1)[0];
+ descriptorPool = descriptorSetLayout.createPool(NUM_FRAMES_IN_FLIGHT, 0, "MatricesPool");
+ descriptorSets = descriptorPool.allocate(NUM_FRAMES_IN_FLIGHT);
var descriptorWrites = VkWriteDescriptorSet.calloc(1, stack);
- boiler.descriptors.writeBuffer(
- stack, descriptorWrites, descriptorSet,
- 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, matrixBuffer.fullRange()
- );
- vkUpdateDescriptorSets(boiler.vkDevice(), descriptorWrites, null);
+ for (int index = 0; index < NUM_FRAMES_IN_FLIGHT; index++) {
+ boiler.descriptors.writeBuffer(
+ stack, descriptorWrites, descriptorSets[index],
+ 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, matrixBuffers[index].fullRange()
+ );
+ vkUpdateDescriptorSets(boiler.vkDevice(), descriptorWrites, null);
+ }
var pushConstants = VkPushConstantRange.calloc(1, stack);
pushConstants.stageFlags(VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT);
@@ -243,7 +252,7 @@ public static void main(String[] args) throws InterruptedException {
class HelloSessionLoop extends SessionLoop {
- private Matrix4f leftHandMatrix, rightHandMatrix;
+ private int frameIndex;
public HelloSessionLoop(
VkbSession session, XrSpace renderSpace,
@@ -273,22 +282,29 @@ protected void handleEvent(XrEventDataBuffer event) {
}
@Override
- protected void prepareRender(MemoryStack stack, XrFrameState frameState) {
- leftHandMatrix = boiler.xr().locateSpace(
+ protected void waitForRenderResources(MemoryStack stack) {
+ frameIndex = (frameIndex + 1) % NUM_FRAMES_IN_FLIGHT;
+ fences[frameIndex].waitIfSubmitted();
+ fences[frameIndex].reset();
+ assertVkSuccess(vkResetCommandPool(
+ xr.boilerInstance.vkDevice(), commandPools[frameIndex], 0
+ ), "ResetCommandPool", "Drawing");
+ }
+
+ @Override
+ protected void recordRenderCommands(
+ MemoryStack stack, XrFrameState frameState, int swapchainImageIndex, Matrix4f[] cameraMatrices
+ ) {
+ Matrix4f leftHandMatrix = boiler.xr().locateSpace(
stack, leftHandSpace, renderSpace, frameState.predictedDisplayTime(), "left hand"
).createMatrix();
if (leftHandMatrix != null) leftHandMatrix.scale(0.1f);
- rightHandMatrix = boiler.xr().locateSpace(
+ Matrix4f rightHandMatrix = boiler.xr().locateSpace(
stack, rightHandSpace, renderSpace, frameState.predictedDisplayTime(), "right hand"
).createMatrix();
if (rightHandMatrix != null) rightHandMatrix.scale(0.1f);
- }
- @Override
- protected void recordRenderCommands(
- MemoryStack stack, int swapchainImageIndex, Matrix4f[] cameraMatrices
- ) {
- var commands = CommandRecorder.begin(commandBuffer, xr.boilerInstance, stack, "Drawing");
+ var commands = CommandRecorder.begin(commandBuffers[frameIndex], xr.boilerInstance, stack, "Drawing");
var colorAttachments = VkRenderingAttachmentInfoKHR.calloc(1, stack);
commands.simpleColorRenderingAttachment(
@@ -311,19 +327,19 @@ protected void recordRenderCommands(
dynamicRenderingInfo.pColorAttachments(colorAttachments);
dynamicRenderingInfo.pDepthAttachment(depthAttachment);
- vkCmdBeginRenderingKHR(commandBuffer, dynamicRenderingInfo);
- vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline);
+ vkCmdBeginRenderingKHR(commandBuffers[frameIndex], dynamicRenderingInfo);
+ vkCmdBindPipeline(commandBuffers[frameIndex], VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline);
vkCmdBindDescriptorSets(
- commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS,
- pipelineLayout, 0, stack.longs(descriptorSet), null
+ commandBuffers[frameIndex], VK_PIPELINE_BIND_POINT_GRAPHICS,
+ pipelineLayout, 0, stack.longs(descriptorSets[frameIndex]), null
);
vkCmdBindVertexBuffers(
- commandBuffer, 0,
+ commandBuffers[frameIndex], 0,
stack.longs(vertexBuffer.vkBuffer()), stack.longs(0)
);
- vkCmdBindIndexBuffer(commandBuffer, indexBuffer.vkBuffer(), 0, VK_INDEX_TYPE_UINT32);
+ vkCmdBindIndexBuffer(commandBuffers[frameIndex], indexBuffer.vkBuffer(), 0, VK_INDEX_TYPE_UINT32);
- var hostMatrixBuffer = memFloatBuffer(matrixBuffer.hostAddress(), 5 * 16);
+ var hostMatrixBuffer = memFloatBuffer(matrixBuffers[frameIndex].hostAddress(), 5 * 16);
cameraMatrices[0].get(0, hostMatrixBuffer);
cameraMatrices[1].get(16, hostMatrixBuffer);
@@ -342,10 +358,10 @@ protected void recordRenderCommands(
pushConstants.put(0, 0);
pushConstants.put(1, 0);
vkCmdPushConstants(
- commandBuffer, pipelineLayout,
+ commandBuffers[frameIndex], pipelineLayout,
VK_SHADER_STAGE_FRAGMENT_BIT | VK_SHADER_STAGE_VERTEX_BIT, 0, pushConstants
);
- vkCmdDrawIndexed(commandBuffer, 3, 1, 0, 0, 0);
+ vkCmdDrawIndexed(commandBuffers[frameIndex], 3, 1, 0, 0, 0);
if (leftHandMatrix != null) {
var giLeftClick = session.prepareSubactionState(stack, handClickAction, pathLeftHand);
@@ -353,10 +369,10 @@ protected void recordRenderCommands(
pushConstants.put(0, holdsLeft ? 0 : 1);
pushConstants.put(1, 1);
vkCmdPushConstants(
- commandBuffer, pipelineLayout,
+ commandBuffers[frameIndex], pipelineLayout,
VK_SHADER_STAGE_FRAGMENT_BIT | VK_SHADER_STAGE_VERTEX_BIT, 0, pushConstants
);
- vkCmdDrawIndexed(commandBuffer, 12, 1, 3, 0, 0);
+ vkCmdDrawIndexed(commandBuffers[frameIndex], 12, 1, 3, 0, 0);
}
if (rightHandMatrix != null) {
var giRightClick = session.prepareSubactionState(stack, handClickAction, pathRightHand);
@@ -364,33 +380,28 @@ protected void recordRenderCommands(
pushConstants.put(0, holdsRight ? 0 : 1);
pushConstants.put(1, 2);
vkCmdPushConstants(
- commandBuffer, pipelineLayout,
+ commandBuffers[frameIndex], pipelineLayout,
VK_SHADER_STAGE_FRAGMENT_BIT | VK_SHADER_STAGE_VERTEX_BIT, 0, pushConstants
);
- vkCmdDrawIndexed(commandBuffer, 12, 1, 3, 0, 0);
+ vkCmdDrawIndexed(commandBuffers[frameIndex], 12, 1, 3, 0, 0);
}
- vkCmdEndRenderingKHR(commandBuffer);
+ vkCmdEndRenderingKHR(commandBuffers[frameIndex]);
commands.end();
}
@Override
- protected void submitAndWaitRender() {
+ protected void submitRenderCommands() {
xr.boilerInstance.queueFamilies().graphics().first().submit(
- commandBuffer, "Drawing", null, fence
+ commandBuffers[frameIndex], "Drawing", null, fences[frameIndex]
);
-
- fence.waitAndReset();
- assertVkSuccess(vkResetCommandPool(
- xr.boilerInstance.vkDevice(), commandPool, 0
- ), "ResetCommandPool", "Drawing");
}
}
new HelloSessionLoop(session, renderSpace, swapchain, width, height).run();
- boiler.sync.fenceBank.returnFence(fence);
- vkDestroyCommandPool(boiler.vkDevice(), commandPool, null);
+ boiler.sync.fenceBank.returnFences(fences);
+ for (var commandPool : commandPools) vkDestroyCommandPool(boiler.vkDevice(), commandPool, null);
vkDestroyPipeline(boiler.vkDevice(), graphicsPipeline, null);
vkDestroyPipelineLayout(boiler.vkDevice(), pipelineLayout, null);
descriptorPool.destroy();
@@ -401,7 +412,7 @@ protected void submitAndWaitRender() {
vertexBuffer.destroy(boiler);
indexBuffer.destroy(boiler);
- matrixBuffer.destroy(boiler);
+ for (var matrixBuffer : matrixBuffers) matrixBuffer.destroy(boiler);
vkDestroyImageView(boiler.vkDevice(), depthImage.vkImageView(), null);
vmaDestroyImage(boiler.vmaAllocator(), depthImage.vkImage(), depthImage.vmaAllocation());
diff --git a/src/main/java/com/github/knokko/boiler/xr/SessionLoop.java b/src/main/java/com/github/knokko/boiler/xr/SessionLoop.java
index d3c2f43..59e0c64 100644
--- a/src/main/java/com/github/knokko/boiler/xr/SessionLoop.java
+++ b/src/main/java/com/github/knokko/boiler/xr/SessionLoop.java
@@ -96,13 +96,7 @@ public void run() {
}
if (this.state == XR_SESSION_STATE_READY && !isRunning) {
- var biSession = XrSessionBeginInfo.calloc(stack);
- biSession.type$Default();
- biSession.primaryViewConfigurationType(getViewConfigurationType());
-
- assertXrSuccess(xrBeginSession(
- session.xrSession, biSession
- ), "BeginSession", null);
+ session.begin(getViewConfigurationType(), "SessionLoop");
isRunning = true;
continue;
}
@@ -110,6 +104,8 @@ public void run() {
if (this.state == XR_SESSION_STATE_SYNCHRONIZED || this.state == XR_SESSION_STATE_VISIBLE ||
this.state == XR_SESSION_STATE_FOCUSED || this.state == XR_SESSION_STATE_READY
) {
+ waitForRenderResources(stack);
+
var frameState = XrFrameState.calloc(stack);
frameState.type$Default();
@@ -150,17 +146,14 @@ stack, renderSpace, numViews, getViewConfigurationType(),
lastCameraMatrix = cameraMatrices;
- syncActions(stack);
-
- prepareRender(stack, frameState);
-
IntBuffer pImageIndex = stack.callocInt(1);
assertXrSuccess(xrAcquireSwapchainImage(
swapchain, null, pImageIndex
), "AcquireSwapchainImage", null);
int swapchainImageIndex = pImageIndex.get(0);
- recordRenderCommands(stack, swapchainImageIndex, cameraMatrices);
+ syncActions(stack);
+ recordRenderCommands(stack, frameState, swapchainImageIndex, cameraMatrices);
var wiSwapchain = XrSwapchainImageWaitInfo.calloc(stack);
wiSwapchain.type$Default();
@@ -170,7 +163,7 @@ stack, renderSpace, numViews, getViewConfigurationType(),
swapchain, wiSwapchain
), "WaitSwapchainImage", null);
- submitAndWaitRender();
+ submitRenderCommands();
assertXrSuccess(xrReleaseSwapchainImage(
swapchain, null
@@ -273,9 +266,11 @@ protected long getSwapchainWaitTimeout() {
protected abstract void handleEvent(XrEventDataBuffer event);
- protected abstract void prepareRender(MemoryStack stack, XrFrameState frameState);
+ protected abstract void waitForRenderResources(MemoryStack stack);
- protected abstract void recordRenderCommands(MemoryStack stack, int swapchainImageIndex, Matrix4f[] cameraMatrices);
+ protected abstract void recordRenderCommands(
+ MemoryStack stack, XrFrameState frameState, int swapchainImageIndex, Matrix4f[] cameraMatrices
+ );
- protected abstract void submitAndWaitRender();
+ protected abstract void submitRenderCommands();
}
diff --git a/src/main/java/com/github/knokko/boiler/xr/VkbSession.java b/src/main/java/com/github/knokko/boiler/xr/VkbSession.java
index 24ced15..d406b72 100644
--- a/src/main/java/com/github/knokko/boiler/xr/VkbSession.java
+++ b/src/main/java/com/github/knokko/boiler/xr/VkbSession.java
@@ -189,6 +189,10 @@ public XrCompositionLayerProjectionView.Buffer createProjectionViews(
return projectionViews;
}
+ /**
+ * Calls xrBeginSession to begin a session, using the given viewConfiguration as
+ * primaryViewConfigurationType
+ */
public void begin(int viewConfiguration, String context) {
try (var stack = stackPush()) {
var biSession = XrSessionBeginInfo.calloc(stack);
diff --git a/src/main/java/com/github/knokko/boiler/xr/XrBoiler.java b/src/main/java/com/github/knokko/boiler/xr/XrBoiler.java
index 814ad11..d613a8e 100644
--- a/src/main/java/com/github/knokko/boiler/xr/XrBoiler.java
+++ b/src/main/java/com/github/knokko/boiler/xr/XrBoiler.java
@@ -153,8 +153,6 @@ public Matrix4f createProjectionMatrix(XrFovf fov, float nearZ, float farZ) {
);
}
-
-
public void pollEvents(MemoryStack stack, String context, Consumer processEvent) {
var eventData = XrEventDataBuffer.malloc(stack);
diff --git a/src/test/java/com/github/knokko/boiler/pipelines/TestDynamicRendering.java b/src/test/java/com/github/knokko/boiler/pipelines/TestDynamicRendering.java
index 8f5994f..72c6590 100644
--- a/src/test/java/com/github/knokko/boiler/pipelines/TestDynamicRendering.java
+++ b/src/test/java/com/github/knokko/boiler/pipelines/TestDynamicRendering.java
@@ -163,7 +163,7 @@ public void testDynamicDepthAttachment() {
);
var depthAttachment = recorder.simpleDepthRenderingAttachment(
- stack, image.vkImageView(), VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL,
+ image.vkImageView(), VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL,
VK_ATTACHMENT_STORE_OP_STORE, 0.75f, 0
);
recorder.beginSimpleDynamicRendering(width, height, null, depthAttachment, null);