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);