Skip to content

Commit

Permalink
Add SingleTimeCommands
Browse files Browse the repository at this point in the history
  • Loading branch information
knokko committed Oct 24, 2024
1 parent 4ac4345 commit eaeeb67
Show file tree
Hide file tree
Showing 10 changed files with 270 additions and 191 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ var commandBuffer = boiler.commands.createPrimaryBuffers(
commandPool, 1, "Copy"
)[0];
```
Or this
```java
var commands = new SingleTimeCommands(boiler);
```
This library provides [plenty](docs/methods.md) of such convenience methods. By using them all whenever
possible, you can dramatically reduce the amount of code you need (at least,
I did when I started using this in my own projects).
Expand Down
18 changes: 18 additions & 0 deletions docs/methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,24 @@ recording a command buffer, or `CommandRecorder.alreadyRecording(...)`
to wrap a command buffer that is already being recorded. Use code
completion and/or the source code to explore all possible options.

### SingleTimeCommands
The `SingleTimeCommands` class is the recommend way to execute
one-time-submit commands. Using it is as simple as
```java
var commands = new SingleTimeCommands(boiler);
commands.submit("Example", recorder -> {
recorder.copyBufferRanges(...); // Just an example
// Or do something with recorder.commandBuffer
// And note that you can also use recorder.stack
}).awaitCompletion(); // The awaitCompletion() is optional
// Optional: reuse this instance later with different commands
commands.destroy();
```
This could spare you all the boilerplate code of creating the
command pool, allocating the command buffer, beginning the
command buffer, ending the command buffer, submitting the
command buffer, and awaiting its fence.

## Culling
The `FrustumCuller` class can be used to test whether a given
camera can see a given `AABB` (axis-aligned bounding box),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package com.github.knokko.boiler.commands;

import com.github.knokko.boiler.BoilerInstance;
import com.github.knokko.boiler.queues.VkbQueueFamily;
import com.github.knokko.boiler.synchronization.FenceSubmission;
import org.lwjgl.vulkan.VkCommandBuffer;

import java.util.function.Consumer;

import static com.github.knokko.boiler.exceptions.VulkanFailureException.assertVkSuccess;
import static org.lwjgl.system.MemoryStack.stackPush;
import static org.lwjgl.vulkan.VK10.*;

/**
* A helper class to easily record and submit one-time-use commands. When you create an instance of this class, it will
* <ol>
* <li>Create a command pool with the <i>VK_COMMAND_POOL_CREATE_TRANSIENT_BIT</i> flag.</li>
* <li>Allocate a command buffer.</li>
* </ol>
* When you call the submit method, it will
* <ol>
* <li>
* If the instance has been used before,
* wait until the previous submission has finished, and reset the command pool.
* </li>
* <li>If validation is enabled, update the debug names of the command pool and command buffer.</li>
* <li>Call <i>vkBeginCommandBuffer</i> with the <i>VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT</i> flag.</li>
* <li>Run your callback.</li>
* <li>Call <i>vkEndCommandBuffer</i>.</li>
* <li>Borrow a <i>VkbFence</i> from the fence bank</li>
* <li>Submit the command buffer with the borrowed fence</li>
* <li>Return the <i>VkbFence</i> to the fence bank</li>
* <li>Return the <i>FenceSubmission</i> to the caller</li>
* </ol>
* When you call the destroy method, it will:
* <ol>
* <li>Await the last submission (if applicable)</li>
* <li>Destroy the command pool</li>
* </ol>
* Thus, this class takes care of all the boilerplate, and the application only needs to handle the actual
* <i>vkCmd**</i> commands.
*/
public class SingleTimeCommands {

private final BoilerInstance instance;
private final VkbQueueFamily queueFamily;
private final long commandPool;
private final VkCommandBuffer commandBuffer;

private FenceSubmission lastSubmission;

/**
* @param instance The <i>BoilerInstance</i>
* @param queueFamily The queue family to which command buffer will be submitted. This class will always use the
* first queue of the family.
*/
public SingleTimeCommands(BoilerInstance instance, VkbQueueFamily queueFamily) {
this.instance = instance;
this.queueFamily = queueFamily;

this.commandPool = instance.commands.createPool(
VK_COMMAND_POOL_CREATE_TRANSIENT_BIT, queueFamily.index(), "SingleTimeCommands"
);
this.commandBuffer = instance.commands.createPrimaryBuffers(commandPool, 1, "SingleTimeCommands")[0];
}

/**
* Creates an instance that will submit the command buffers to the first queue of the graphics queue family
*/
public SingleTimeCommands(BoilerInstance instance) {
this(instance, instance.queueFamilies().graphics());
}

/**
* <ol>
* <li>
* If this method has been called before,
* waits until the previous submission has finished, and reset the command pool.
* </li>
* <li>If validation is enabled, updates the debug names of the command pool and command buffer.</li>
* <li>Calls <i>vkBeginCommandBuffer</i> with the <i>VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT</i> flag.</li>
* <li>Runs your <i>recordCommands</i> callback.</li>
* <li>Calls <i>vkEndCommandBuffer</i>.</li>
* <li>Borrows a <i>VkbFence</i> from the fence bank</li>
* <li>Submits the command buffer with the borrowed fence</li>
* <li>Returns the <i>VkbFence</i> to the fence bank</li>
* <li>Returns the <i>FenceSubmission</i> to the caller</li>
* </ol>
*
* <p>
* Note that this method does <b>not</b> wait until the submission has finished. If you want this, you need to call
* the <i>awaitCompletion()</i> method of the returned <i>FenceSubmission</i>.
* </p>
*
* Note that this method is <i>synchronized</i>, so you can safely call it from multiple threads. It could however
* slow the second thread down since it will be blocked until the submission of the first thread is finished.
* If this is a potential problem, considering using multiple instances of this class.
* @param context The debug name that will be given to the command pool/buffer, if validation is enabled
* @param recordCommands The callback that records the actual commands
* @return The command submission, which you can await.
*/
public synchronized FenceSubmission submit(String context, Consumer<CommandRecorder> recordCommands) {
try (var stack = stackPush()) {
if (lastSubmission != null) {
lastSubmission.awaitCompletion();
assertVkSuccess(vkResetCommandPool(
instance.vkDevice(), commandPool, 0
), "ResetCommandPool", context);
}

instance.debug.name(stack, commandPool, VK_OBJECT_TYPE_COMMAND_POOL, context);
instance.debug.name(stack, commandBuffer.address(), VK_OBJECT_TYPE_COMMAND_BUFFER, context);

var recorder = CommandRecorder.begin(
commandBuffer, instance, stack,
VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, context
);
recordCommands.accept(recorder);
recorder.end();

var fence = instance.sync.fenceBank.borrowFence(false, context);
lastSubmission = queueFamily.first().submit(commandBuffer, context, null, fence);
instance.sync.fenceBank.returnFence(fence);
return lastSubmission;
}
}

/**
* Waits until the last command submission has finished (if applicable), and then destroys the command pool.
*/
public void destroy() {
if (lastSubmission != null) lastSubmission.awaitCompletion();
vkDestroyCommandPool(instance.vkDevice(), commandPool, null);
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package com.github.knokko.boiler.buffers;

import com.github.knokko.boiler.builders.BoilerBuilder;
import com.github.knokko.boiler.commands.CommandRecorder;
import com.github.knokko.boiler.commands.SingleTimeCommands;
import com.github.knokko.boiler.synchronization.ResourceUsage;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.lwjgl.system.MemoryStack.stackPush;
import static org.lwjgl.system.MemoryUtil.memByteBuffer;
import static org.lwjgl.vulkan.VK10.*;

Expand Down Expand Up @@ -34,33 +33,18 @@ public void testBufferCopies() {
sourceHostBuffer.put((byte) index);
}

try (var stack = stackPush()) {
var fence = instance.sync.fenceBank.borrowFence(false, "Copying");
var commandPool = instance.commands.createPool(
0, instance.queueFamilies().graphics().index(), "Copy"
);
var commandBuffer = instance.commands.createPrimaryBuffers(
commandPool, 1, "Copy"
)[0];

var recorder = CommandRecorder.begin(commandBuffer, instance, stack, "Copying");
var commands = new SingleTimeCommands(instance);
commands.submit("Copying", recorder -> {
recorder.copyBuffer(sourceBuffer.fullRange(), middleBuffer.vkBuffer(), 0);
recorder.bufferBarrier(middleBuffer.fullRange(), ResourceUsage.TRANSFER_DEST, ResourceUsage.TRANSFER_SOURCE);
recorder.copyBuffer(middleBuffer.fullRange(), destinationBuffer.vkBuffer(), 0);
recorder.end();

instance.queueFamilies().graphics().first().submit(
commandBuffer, "Copying", null, fence
);
fence.awaitSignal();
instance.sync.fenceBank.returnFence(fence);
vkDestroyCommandPool(instance.vkDevice(), commandPool, null);
}
}).awaitCompletion();

for (int index = 0; index < 100; index++) {
assertEquals((byte) index, destinationHostBuffer.get());
}

commands.destroy();
sourceBuffer.destroy(instance);
middleBuffer.destroy(instance);
destinationBuffer.destroy(instance);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,50 @@
import static com.github.knokko.boiler.exceptions.VulkanFailureException.assertVkSuccess;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.lwjgl.system.MemoryStack.stackPush;
import static org.lwjgl.system.MemoryUtil.memByteBuffer;
import static org.lwjgl.system.MemoryUtil.memGetByte;
import static org.lwjgl.system.MemoryUtil.*;
import static org.lwjgl.vulkan.VK10.*;
import static org.lwjgl.vulkan.VK12.VK_API_VERSION_1_2;

public class TestCommandRecorder {

@Test
public void testAlreadyRecording() {
var instance = new BoilerBuilder(
VK_API_VERSION_1_0, "TestAlreadyRecording", 1
).validation().forbidValidationErrors().build();

var buffer = instance.buffers.createMapped(
8, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, "TestBuffer"
);
memPutInt(buffer.hostAddress(), 1234);
var commandPool = instance.commands.createPool(0, instance.queueFamilies().graphics().index(), "CopyPool");
var commandBuffer = instance.commands.createPrimaryBuffers(commandPool, 1, "CopyCommandBuffer")[0];
var fence = instance.sync.fenceBank.borrowFence(false, "CopyFence");
try (var stack = stackPush()) {

var biCommands = VkCommandBufferBeginInfo.calloc(stack);
biCommands.sType$Default();
biCommands.flags(0);
biCommands.pInheritanceInfo(null);
assertVkSuccess(vkBeginCommandBuffer(
commandBuffer, biCommands
), "BeginCommandBuffer", "Copying");
var recorder = CommandRecorder.alreadyRecording(commandBuffer, instance, stack);
recorder.copyBuffer(buffer.range(0, 4), buffer.vkBuffer(), 4);
recorder.end("Copying");

instance.queueFamilies().transfer().first().submit(commandBuffer, "Copying", null, fence);
fence.awaitSignal();
}

assertEquals(1234, memGetInt(buffer.hostAddress() + 4));

instance.sync.fenceBank.returnFence(fence);
vkDestroyCommandPool(instance.vkDevice(), commandPool, null);
buffer.destroy(instance);
instance.destroyInitialObjects();
}

@Test
public void testCopyImage() {
var instance = new BoilerBuilder(
Expand All @@ -39,22 +76,8 @@ public void testCopyImage() {
VK_SAMPLE_COUNT_1_BIT, 1, 1, false, "DestImage"
);

var commandPool = instance.commands.createPool(0, instance.queueFamilies().graphics().index(), "CopyPool");
var commandBuffer = instance.commands.createPrimaryBuffers(commandPool, 1, "CopyCommandBuffer")[0];
var fence = instance.sync.fenceBank.borrowFence(false, "CopyFence");

try (var stack = stackPush()) {
var biCommands = VkCommandBufferBeginInfo.calloc(stack);
biCommands.sType$Default();
biCommands.flags(0);
biCommands.pInheritanceInfo(null);

assertVkSuccess(vkBeginCommandBuffer(
commandBuffer, biCommands
), "BeginCommandBuffer", "Copying");

var recorder = CommandRecorder.alreadyRecording(commandBuffer, instance, stack);

var commands = new SingleTimeCommands(instance);
commands.submit("Copying", recorder -> {
recorder.transitionLayout(sourceImage, null, ResourceUsage.TRANSFER_DEST);
recorder.clearColorImage(sourceImage.vkImage(), 0f, 1f, 1f, 1f);
recorder.transitionLayout(sourceImage, ResourceUsage.TRANSFER_DEST, ResourceUsage.TRANSFER_SOURCE);
Expand All @@ -63,20 +86,14 @@ public void testCopyImage() {
recorder.copyImage(sourceImage, destImage);
recorder.transitionLayout(destImage, ResourceUsage.TRANSFER_DEST, ResourceUsage.TRANSFER_SOURCE);
recorder.copyImageToBuffer(destImage, destBuffer.fullRange());
}).awaitCompletion();

recorder.end("Copying");
assertEquals((byte) 0, memGetByte(destBuffer.hostAddress()));
assertEquals((byte) 255, memGetByte(destBuffer.hostAddress() + 1));
assertEquals((byte) 255, memGetByte(destBuffer.hostAddress() + 2));
assertEquals((byte) 255, memGetByte(destBuffer.hostAddress() + 3));

instance.queueFamilies().graphics().first().submit(commandBuffer, "Copying", null, fence);
fence.waitAndReset();

assertEquals((byte) 0, memGetByte(destBuffer.hostAddress()));
assertEquals((byte) 255, memGetByte(destBuffer.hostAddress() + 1));
assertEquals((byte) 255, memGetByte(destBuffer.hostAddress() + 2));
assertEquals((byte) 255, memGetByte(destBuffer.hostAddress() + 3));
}

instance.sync.fenceBank.returnFence(fence);
vkDestroyCommandPool(instance.vkDevice(), commandPool, null);
commands.destroy();
destBuffer.destroy(instance);
sourceImage.destroy(instance);
destImage.destroy(instance);
Expand Down Expand Up @@ -111,13 +128,10 @@ public void testBlitImage() {
VK_SAMPLE_COUNT_1_BIT, 1, 1, false, "DestImage"
);

var commandPool = instance.commands.createPool(0, instance.queueFamilies().graphics().index(), "CopyPool");
var commandBuffer = instance.commands.createPrimaryBuffers(commandPool, 1, "CopyCommandBuffer")[0];
var fence = instance.sync.fenceBank.borrowFence(false, "CopyFence");

try (var stack = stackPush()) {
var recorder = CommandRecorder.begin(commandBuffer, instance, stack, "Blitting");
var hostBuffer = memByteBuffer(buffer.hostAddress(), 4 * width1 * height1);

var commands = new SingleTimeCommands(instance);
commands.submit("Blitting", recorder -> {
recorder.transitionLayout(sourceImage, null, ResourceUsage.TRANSFER_DEST);
recorder.copyBufferToImage(sourceImage, buffer.fullRange());
recorder.transitionLayout(sourceImage, ResourceUsage.TRANSFER_DEST, ResourceUsage.TRANSFER_SOURCE);
Expand All @@ -127,9 +141,7 @@ public void testBlitImage() {

recorder.transitionLayout(destImage, ResourceUsage.TRANSFER_DEST, ResourceUsage.TRANSFER_SOURCE);
recorder.copyImageToBuffer(destImage, buffer.fullRange());
recorder.end("Copying");

var hostBuffer = memByteBuffer(buffer.hostAddress(), 4 * width1 * height1);
// First pixel is (R, G, B, A) = (100, 0, 200, 255)
hostBuffer.put(0, (byte) 100);
hostBuffer.put(1, (byte) 0);
Expand All @@ -143,19 +155,15 @@ public void testBlitImage() {
hostBuffer.put(index + 2, (byte) 0);
hostBuffer.put(index + 3, (byte) 255);
}
}).awaitCompletion();

instance.queueFamilies().graphics().first().submit(commandBuffer, "Copying", null, fence);
fence.waitAndReset();
// So the blitted pixel should be (25, 0, 50, 255)
assertEquals((byte) 25, hostBuffer.get(0));
assertEquals((byte) 0, hostBuffer.get(1));
assertEquals((byte) 50, hostBuffer.get(2));
assertEquals((byte) 255, hostBuffer.get(3));

// So the blitted pixel should be (25, 0, 50, 255)
assertEquals((byte) 25, hostBuffer.get(0));
assertEquals((byte) 0, hostBuffer.get(1));
assertEquals((byte) 50, hostBuffer.get(2));
assertEquals((byte) 255, hostBuffer.get(3));
}

instance.sync.fenceBank.returnFence(fence);
vkDestroyCommandPool(instance.vkDevice(), commandPool, null);
commands.destroy();
buffer.destroy(instance);
sourceImage.destroy(instance);
destImage.destroy(instance);
Expand Down
Loading

0 comments on commit eaeeb67

Please sign in to comment.