Skip to content

Commit

Permalink
Add FixedDescriptorBank and GrowingDescriptorBank
Browse files Browse the repository at this point in the history
  • Loading branch information
knokko committed Nov 15, 2023
1 parent 2928351 commit 9484100
Show file tree
Hide file tree
Showing 4 changed files with 378 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.github.knokko.boiler.descriptors;

import com.github.knokko.boiler.instance.BoilerInstance;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.vulkan.VkDescriptorPoolCreateInfo;
import org.lwjgl.vulkan.VkDescriptorSetAllocateInfo;

import java.util.concurrent.ConcurrentSkipListSet;
import java.util.function.BiConsumer;

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

/**
* <p>
* A <i>FixedDescriptorBank</i> is a descriptor pool from which descriptor sets of one specific descriptor set layout
* can be 'borrowed'. All descriptor sets are allocated right after creation of the descriptor pool, and are never
* freed or reset. Instead, descriptor sets can be 'borrowed' and 'returned', after which they can be 'borrowed' again.
* </p>
*
* <p>
* <i>FixedDescriptorBank</i>s are 'fixed' in the sense that they have a bounded capacity specified in the
* constructor, and can't grow any bigger. You can use <i>GrowingDescriptorBank</i> instead if you don't like this
* limitation.
* </p>
*
* Borrowing and returning descriptor sets is thread-safe, but destroying the bank is not: the bank can only be
* destroyed when all borrows and returns have been completed.
*/
public class FixedDescriptorBank {

private final BoilerInstance instance;
private final long descriptorPool;

private final ConcurrentSkipListSet<Long> borrowedDescriptorSets;
private final ConcurrentSkipListSet<Long> unusedDescriptorSets;

public FixedDescriptorBank(
BoilerInstance instance, long descriptorSetLayout, String context,
BiConsumer<MemoryStack, VkDescriptorPoolCreateInfo> configureDescriptorPool
) {
this.instance = instance;
try (var stack = stackPush()) {
var ciPool = VkDescriptorPoolCreateInfo.calloc(stack);
ciPool.sType$Default();
configureDescriptorPool.accept(stack, ciPool);
int capacity = ciPool.maxSets();

var pPool = stack.callocLong(1);
assertVkSuccess(vkCreateDescriptorPool(
instance.vkDevice(), ciPool, null, pPool
), "CreateDescriptorPool", "DescriptorBank" + context);
this.descriptorPool = pPool.get(0);

var aiSets = VkDescriptorSetAllocateInfo.calloc(stack);
aiSets.sType$Default();
aiSets.descriptorPool(descriptorPool);
aiSets.pSetLayouts(stack.longs(descriptorSetLayout));

var pSets = stack.callocLong(capacity);
assertVkSuccess(vkAllocateDescriptorSets(
instance.vkDevice(), aiSets, pSets
), "AllocateDescriptorSets", "DescriptorBank" + context);

this.unusedDescriptorSets = new ConcurrentSkipListSet<>();
for (int index = 0; index < capacity; index++) {
this.unusedDescriptorSets.add(pSets.get(index));
}
this.borrowedDescriptorSets = new ConcurrentSkipListSet<>();
}
}

/**
* Note: this method returns null when all descriptor sets are currently borrowed.
*/
public Long borrowDescriptorSet() {
Long result = unusedDescriptorSets.pollFirst();
if (result != null) borrowedDescriptorSets.add(result);
return result;
}

public void returnDescriptorSet(long descriptorSet) {
if (!borrowedDescriptorSets.remove(descriptorSet)) {
throw new IllegalArgumentException(descriptorSet + " wasn't borrowed");
}
unusedDescriptorSets.add(descriptorSet);
}

public void destroy(boolean checkEmpty) {
if (checkEmpty && !borrowedDescriptorSets.isEmpty()) {
throw new IllegalStateException("Not all descriptor sets have been returned");
}
vkDestroyDescriptorPool(instance.vkDevice(), descriptorPool, null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package com.github.knokko.boiler.descriptors;

import com.github.knokko.boiler.instance.BoilerInstance;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.vulkan.VkDescriptorPoolCreateInfo;
import org.lwjgl.vulkan.VkDescriptorSetAllocateInfo;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.function.BiConsumer;

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

/**
* <p>
* A <i>GrowingDescriptorBank</i> is a 'bank' of descriptor sets in the sense that users can borrow descriptor sets
* from it, and return those once they are no longer needed. Once allocated, descriptor sets from the bank will
* never be freed or reset: when they are returned, they can simply be borrowed again. When the bank is destroyed,
* all descriptor pools will be destroyed.
* </p>
*
* <p>
* Unlike <i>FixedDescriptorBank</i>s, <i>GrowingDescriptorBank</i>s have unlimited capacity: when all descriptor
* sets have been borrowed, it will create an additional (bigger) descriptor pool. But, just like fixed banks,
* growing banks also support only descriptor sets from 1 descriptor set layout.
* </p>
*
* Borrowing and returning descriptor sets is thread-safe, but destroying the bank is not: the bank can only be
* destroyed when all borrows and returns have been completed.
*/
public class GrowingDescriptorBank {

private final BoilerInstance instance;
private final long descriptorSetLayout;
private final String name;
private final BiConsumer<MemoryStack, VkDescriptorPoolCreateInfo> configureDescriptorPool;

private final List<Long> descriptorPools = new ArrayList<>();
private int nextCapacity = 2;


private final ConcurrentSkipListSet<Long> unusedDescriptorSets, borrowedDescriptorSets;

/**
* @param name Debugging purposes only
* @param configureDescriptorPool Populate the given <i>VkDescriptorPoolCreateInfo</i>. You should use this to
* set <i>pPoolSizes</i> for a descriptor pool that can hold exactly <b>1</b>
* descriptor set. (The <i>descriptorCount</i>s will automatically be multiplied
* with the capacity of the descriptor pool.) You should ignore <i>maxSets</i> and
* you can optionally set the <i>flags</i>.
*/
public GrowingDescriptorBank(
BoilerInstance instance, long descriptorSetLayout, String name,
BiConsumer<MemoryStack, VkDescriptorPoolCreateInfo> configureDescriptorPool
) {
this.instance = instance;
this.descriptorSetLayout = descriptorSetLayout;
this.name = name;
this.configureDescriptorPool = configureDescriptorPool;
this.unusedDescriptorSets = new ConcurrentSkipListSet<>();
this.borrowedDescriptorSets = new ConcurrentSkipListSet<>();
}

public long borrowDescriptorSet() {
Long maybeResult = unusedDescriptorSets.pollFirst();
if (maybeResult == null) {
synchronized (this) {

// Ensure that it's not possible to create 2 new pools at the same time
maybeResult = unusedDescriptorSets.pollFirst();
if (maybeResult == null) {

try (var stack = stackPush()) {
var ciPool = VkDescriptorPoolCreateInfo.calloc(stack);
ciPool.sType$Default();
configureDescriptorPool.accept(stack, ciPool);
ciPool.maxSets(nextCapacity);
for (var poolSize : Objects.requireNonNull(ciPool.pPoolSizes())) {
poolSize.descriptorCount(nextCapacity * poolSize.descriptorCount());
}

var pPool = stack.callocLong(1);
assertVkSuccess(vkCreateDescriptorPool(
instance.vkDevice(), ciPool, null, pPool
), "CreateDescriptorPool", "GrowingDescriptorBank-" + name + "-" + nextCapacity);
long newDescriptorPool = pPool.get(0);

descriptorPools.add(newDescriptorPool);

var pSetLayouts = stack.callocLong(nextCapacity);
for (int index = 0; index < nextCapacity; index++) {
pSetLayouts.put(index, descriptorSetLayout);
}
var aiSets = VkDescriptorSetAllocateInfo.calloc(stack);
aiSets.sType$Default();
aiSets.descriptorPool(newDescriptorPool);
aiSets.pSetLayouts(pSetLayouts);

var pSets = stack.callocLong(nextCapacity);
assertVkSuccess(vkAllocateDescriptorSets(
instance.vkDevice(), aiSets, pSets
), "AllocateDescriptorSets", "GrowingDescriptorBank-" + name + "-" + nextCapacity);

maybeResult = pSets.get(0);
for (int index = 1; index < nextCapacity; index++) {
unusedDescriptorSets.add(pSets.get(index));
}
nextCapacity *= 2;
}
}
}
}
borrowedDescriptorSets.add(maybeResult);
return maybeResult;
}

public void returnDescriptorSet(long descriptorSet) {
if (!borrowedDescriptorSets.remove(descriptorSet)) {
throw new IllegalArgumentException("Descriptor set " + descriptorSet + " wasn't borrowed");
}
unusedDescriptorSets.add(descriptorSet);
}

public void destroy(boolean checkBorrows) {
if (checkBorrows && !borrowedDescriptorSets.isEmpty()) {
throw new IllegalStateException("Not all borrowed descriptor sets have been returned");
}
for (long descriptorPool : descriptorPools) {
vkDestroyDescriptorPool(instance.vkDevice(), descriptorPool, null);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.github.knokko.boiler.descriptors;

import com.github.knokko.boiler.builder.BoilerBuilder;
import com.github.knokko.boiler.builder.instance.ValidationFeatures;
import org.junit.jupiter.api.Test;
import org.lwjgl.vulkan.VkDescriptorPoolSize;
import org.lwjgl.vulkan.VkDescriptorSetLayoutBinding;

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

public class TestFixedDescriptorBank {

@Test
public void testFixedDescriptorBank() {
var boiler = new BoilerBuilder(VK_API_VERSION_1_0, "TestFixedDescriptorBank", 1)
.validation(new ValidationFeatures(false, false, false, false, false))
.build();

long descriptorSetLayout;
try (var stack = stackPush()) {
var bindings = VkDescriptorSetLayoutBinding.calloc(1, stack);
bindings.binding(0);
bindings.descriptorType(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER);
bindings.descriptorCount(5);
bindings.stageFlags(VK_SHADER_STAGE_VERTEX_BIT);

descriptorSetLayout = boiler.descriptors.createLayout(stack, bindings, "Test");
}
var bank = new FixedDescriptorBank(boiler, descriptorSetLayout, "Test", (stack, ciPool) -> {
var poolSizes = VkDescriptorPoolSize.calloc(1, stack);
poolSizes.type(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER);
poolSizes.descriptorCount(5);

ciPool.flags(0);
ciPool.maxSets(2);
ciPool.pPoolSizes(poolSizes);
});

long descriptorSet1 = bank.borrowDescriptorSet();
long descriptorSet2 = bank.borrowDescriptorSet();
assertNotEquals(descriptorSet1, descriptorSet2);
assertNull(bank.borrowDescriptorSet());

bank.returnDescriptorSet(descriptorSet1);
assertEquals(descriptorSet1, bank.borrowDescriptorSet());

bank.returnDescriptorSet(descriptorSet2);
assertEquals(descriptorSet2, bank.borrowDescriptorSet());

assertNull(bank.borrowDescriptorSet());

bank.returnDescriptorSet(descriptorSet1);
bank.returnDescriptorSet(descriptorSet2);

long descriptorSet12 = bank.borrowDescriptorSet();
long descriptorSet21 = bank.borrowDescriptorSet();
assertNull(bank.borrowDescriptorSet());
assertNotEquals(descriptorSet12, descriptorSet21);
assertTrue(descriptorSet12 == descriptorSet1 || descriptorSet12 == descriptorSet2);
assertTrue(descriptorSet21 == descriptorSet1 || descriptorSet21 == descriptorSet2);

bank.destroy(false);

vkDestroyDescriptorSetLayout(boiler.vkDevice(), descriptorSetLayout, null);
boiler.destroyInitialObjects();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.github.knokko.boiler.descriptors;

import com.github.knokko.boiler.builder.BoilerBuilder;
import com.github.knokko.boiler.builder.instance.ValidationFeatures;
import org.junit.jupiter.api.Test;
import org.lwjgl.vulkan.VkDescriptorPoolSize;
import org.lwjgl.vulkan.VkDescriptorSetLayoutBinding;

import java.util.HashSet;
import java.util.Set;

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

public class TestGrowingDescriptorBank {

@Test
public void testGrowingDescriptorBank() {
var boiler = new BoilerBuilder(VK_API_VERSION_1_0, "TestFixedDescriptorBank", 1)
.validation(new ValidationFeatures(false, false, false, false, false))
.build();

long descriptorSetLayout;
try (var stack = stackPush()) {
var bindings = VkDescriptorSetLayoutBinding.calloc(1, stack);
bindings.binding(0);
bindings.descriptorType(VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE);
bindings.descriptorCount(3);
bindings.stageFlags(VK_SHADER_STAGE_VERTEX_BIT);

descriptorSetLayout = boiler.descriptors.createLayout(stack, bindings, "Test");
}

var bank = new GrowingDescriptorBank(boiler, descriptorSetLayout, "Test", (stack, ciPool) -> {
var poolSizes = VkDescriptorPoolSize.calloc(1, stack);
poolSizes.type(VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE);
poolSizes.descriptorCount(3);

ciPool.pPoolSizes(poolSizes);
});

var initialSets = new long[15];
for (int index = 0; index < initialSets.length; index++) {
initialSets[index] = bank.borrowDescriptorSet();
}
assertUnique(initialSets);

for (int index = 5; index < 12; index++) {
bank.returnDescriptorSet(initialSets[index]);
}

var finalSets = new HashSet<Long>();
for (int index = 0; index < 200; index++) {
finalSets.add(bank.borrowDescriptorSet());
}
assertEquals(200, finalSets.size());

for (int index = 0; index < initialSets.length; index++) {
if (index >= 5 && index < 12) {
assertTrue(finalSets.contains(initialSets[index]));
} else {
assertFalse(finalSets.contains(initialSets[index]));
}
}

bank.destroy(false);
vkDestroyDescriptorSetLayout(boiler.vkDevice(), descriptorSetLayout, null);
boiler.destroyInitialObjects();
}

private void assertUnique(long[] array) {
Set<Long> set = new HashSet<>(array.length);
for (long element : array) set.add(element);
assertEquals(set.size(), array.length);
}
}

0 comments on commit 9484100

Please sign in to comment.