Compare commits

..

2 Commits

Author SHA1 Message Date
burkkyy ab7c850121 Move barriers and frame submit to synchronization2
Image barriers now carry their own stage masks and are recorded with
pipelineBarrier2, so barriers flushed together no longer share the
union of their stages. AccessState uses the 64-bit stage and access
flags, and the frame submit uses submit2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:51:33 -07:00
burkkyy 1ba1442cc4 Require GPU-driven device features
Enable descriptor indexing (bindless), buffer device address, scalar
block layout, indirect draw count, shader draw parameters, timeline
semaphores and synchronization2 alongside dynamic rendering. A single
feature list drives both physical device selection and device creation.
The VMA allocator is created with buffer device address support.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:51:33 -07:00
7 changed files with 169 additions and 69 deletions
+18 -12
View File
@@ -86,22 +86,28 @@ void RenderContext::endFrame() {
FrameData& frame = this->frames[this->currentFrameIndex];
frame.commandBuffer.end();
vk::Semaphore waitSemaphore = frame.imageAvailable.get();
vk::Semaphore signalSemaphore =
this->renderFinishedSemaphores.at(this->currentImageIndex).get();
vk::PipelineStageFlags waitStage = SWAPCHAIN_ACQUIRE_STAGE;
vk::CommandBuffer commandBuffer = frame.commandBuffer;
vk::SubmitInfo submitInfo{};
submitInfo.waitSemaphoreCount = 1;
submitInfo.pWaitSemaphores = &waitSemaphore;
submitInfo.pWaitDstStageMask = &waitStage;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffer;
submitInfo.signalSemaphoreCount = 1;
submitInfo.pSignalSemaphores = &signalSemaphore;
vk::SemaphoreSubmitInfo waitInfo{};
waitInfo.semaphore = frame.imageAvailable.get();
waitInfo.stageMask = SWAPCHAIN_ACQUIRE_STAGE;
this->device.getGraphicsQueue().submit(submitInfo, frame.inFlight.get());
// Present has no pipeline stage to wait on, so signal once everything in
// the submission is done.
vk::SemaphoreSubmitInfo signalInfo{};
signalInfo.semaphore = signalSemaphore;
signalInfo.stageMask = vk::PipelineStageFlagBits2::eAllCommands;
vk::CommandBufferSubmitInfo commandBufferInfo{};
commandBufferInfo.commandBuffer = frame.commandBuffer;
vk::SubmitInfo2 submitInfo{};
submitInfo.setWaitSemaphoreInfos(waitInfo);
submitInfo.setCommandBufferInfos(commandBufferInfo);
submitInfo.setSignalSemaphoreInfos(signalInfo);
this->device.getGraphicsQueue().submit2(submitInfo, frame.inFlight.get());
vk::Result result = this->swapchain->present(
this->device.getPresentQueue(), this->currentImageIndex, signalSemaphore);
+1 -8
View File
@@ -55,8 +55,6 @@ void RenderGraph::transitionTo(ImageRef ref, const AccessState& next,
pending.barriers.push_back(makeImageBarrier(
imported.image->get(), imported.image->getSubresourceRange(),
imported.state, next));
pending.srcStages |= imported.state.stage;
pending.dstStages |= next.stage;
imported.state = next;
} else {
// Same-layout read after read. Nothing to record, but a later write has
@@ -68,12 +66,7 @@ void RenderGraph::transitionTo(ImageRef ref, const AccessState& next,
void RenderGraph::flush(vk::CommandBuffer commandBuffer,
PendingBarriers& pending) {
if (pending.barriers.empty()) {
return;
}
commandBuffer.pipelineBarrier(pending.srcStages, pending.dstStages, {},
nullptr, nullptr, pending.barriers);
recordImageBarriers(commandBuffer, pending.barriers);
pending = PendingBarriers{};
}
+1 -3
View File
@@ -104,9 +104,7 @@ class RenderGraph {
};
struct PendingBarriers {
std::vector<vk::ImageMemoryBarrier> barriers;
vk::PipelineStageFlags srcStages;
vk::PipelineStageFlags dstStages;
std::vector<vk::ImageMemoryBarrier2> barriers;
};
void transitionTo(ImageRef ref, const AccessState& next,
+49 -30
View File
@@ -14,10 +14,11 @@ constexpr std::array<Access, 7> ALL_ACCESSES = {
Access::Present,
};
constexpr vk::AccessFlags WRITE_ACCESSES =
vk::AccessFlagBits::eColorAttachmentWrite |
vk::AccessFlagBits::eDepthStencilAttachmentWrite |
vk::AccessFlagBits::eShaderWrite | vk::AccessFlagBits::eTransferWrite;
constexpr vk::AccessFlags2 WRITE_ACCESSES =
vk::AccessFlagBits2::eColorAttachmentWrite |
vk::AccessFlagBits2::eDepthStencilAttachmentWrite |
vk::AccessFlagBits2::eShaderStorageWrite |
vk::AccessFlagBits2::eTransferWrite;
} // namespace
@@ -25,34 +26,37 @@ AccessState getAccessState(Access access) {
switch (access) {
case Access::ColorWrite:
return {vk::ImageLayout::eColorAttachmentOptimal,
vk::PipelineStageFlagBits::eColorAttachmentOutput,
vk::AccessFlagBits::eColorAttachmentRead |
vk::AccessFlagBits::eColorAttachmentWrite};
vk::PipelineStageFlagBits2::eColorAttachmentOutput,
vk::AccessFlagBits2::eColorAttachmentRead |
vk::AccessFlagBits2::eColorAttachmentWrite};
case Access::DepthWrite:
return {vk::ImageLayout::eDepthStencilAttachmentOptimal,
vk::PipelineStageFlagBits::eEarlyFragmentTests |
vk::PipelineStageFlagBits::eLateFragmentTests,
vk::AccessFlagBits::eDepthStencilAttachmentRead |
vk::AccessFlagBits::eDepthStencilAttachmentWrite};
vk::PipelineStageFlagBits2::eEarlyFragmentTests |
vk::PipelineStageFlagBits2::eLateFragmentTests,
vk::AccessFlagBits2::eDepthStencilAttachmentRead |
vk::AccessFlagBits2::eDepthStencilAttachmentWrite};
case Access::SampledRead:
return {vk::ImageLayout::eShaderReadOnlyOptimal,
vk::PipelineStageFlagBits::eFragmentShader,
vk::AccessFlagBits::eShaderRead};
vk::PipelineStageFlagBits2::eFragmentShader,
vk::AccessFlagBits2::eShaderSampledRead};
case Access::StorageReadWrite:
return {
vk::ImageLayout::eGeneral, vk::PipelineStageFlagBits::eComputeShader,
vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eShaderWrite};
return {vk::ImageLayout::eGeneral,
vk::PipelineStageFlagBits2::eComputeShader,
vk::AccessFlagBits2::eShaderStorageRead |
vk::AccessFlagBits2::eShaderStorageWrite};
case Access::TransferSrc:
return {vk::ImageLayout::eTransferSrcOptimal,
vk::PipelineStageFlagBits::eTransfer,
vk::AccessFlagBits::eTransferRead};
vk::PipelineStageFlagBits2::eTransfer,
vk::AccessFlagBits2::eTransferRead};
case Access::TransferDst:
return {vk::ImageLayout::eTransferDstOptimal,
vk::PipelineStageFlagBits::eTransfer,
vk::AccessFlagBits::eTransferWrite};
vk::PipelineStageFlagBits2::eTransfer,
vk::AccessFlagBits2::eTransferWrite};
case Access::Present:
// Presentation isn't a pipeline stage; the present waits on a semaphore
// instead, so the barrier into this state has nothing to block.
return {vk::ImageLayout::ePresentSrcKHR,
vk::PipelineStageFlagBits::eBottomOfPipe, vk::AccessFlags{}};
vk::PipelineStageFlagBits2::eNone, vk::AccessFlags2{}};
}
log::fatal("Unknown Access value.");
@@ -83,22 +87,37 @@ bool needsBarrier(const AccessState& from, const AccessState& to) {
return from.layout != to.layout || writesImage(from) || writesImage(to);
}
vk::ImageMemoryBarrier makeImageBarrier(vk::Image image,
const vk::ImageSubresourceRange& range,
const AccessState& from,
const AccessState& to) {
vk::ImageMemoryBarrier barrier{};
vk::ImageMemoryBarrier2 makeImageBarrier(vk::Image image,
const vk::ImageSubresourceRange& range,
const AccessState& from,
const AccessState& to) {
vk::ImageMemoryBarrier2 barrier{};
barrier.srcStageMask = from.stage;
barrier.dstStageMask = to.stage;
// Only writes need to be made available; reads just need the execution
// dependency the stage masks already give.
barrier.srcAccessMask = from.access & WRITE_ACCESSES;
barrier.dstAccessMask = to.access;
barrier.oldLayout = from.layout;
barrier.newLayout = to.layout;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.image = image;
barrier.subresourceRange = range;
// Only writes need to be made available; reads just need the execution
// dependency the stage masks already give.
barrier.srcAccessMask = from.access & WRITE_ACCESSES;
barrier.dstAccessMask = to.access;
return barrier;
}
void recordImageBarriers(
vk::CommandBuffer commandBuffer,
vk::ArrayProxy<const vk::ImageMemoryBarrier2> barriers) {
if (barriers.empty()) {
return;
}
vk::DependencyInfo dependency{};
dependency.imageMemoryBarrierCount = barriers.size();
dependency.pImageMemoryBarriers = barriers.data();
commandBuffer.pipelineBarrier2(dependency);
}
} // namespace sophia
+16 -8
View File
@@ -25,8 +25,8 @@ enum class Access {
*/
struct AccessState {
vk::ImageLayout layout = vk::ImageLayout::eUndefined;
vk::PipelineStageFlags stage = vk::PipelineStageFlagBits::eTopOfPipe;
vk::AccessFlags access;
vk::PipelineStageFlags2 stage = vk::PipelineStageFlagBits2::eNone;
vk::AccessFlags2 access;
};
/**
@@ -35,8 +35,8 @@ struct AccessState {
* just-acquired state has to use this as its source stage, otherwise the
* layout transition isn't ordered after the acquire.
*/
inline constexpr vk::PipelineStageFlags SWAPCHAIN_ACQUIRE_STAGE =
vk::PipelineStageFlagBits::eColorAttachmentOutput;
inline constexpr vk::PipelineStageFlags2 SWAPCHAIN_ACQUIRE_STAGE =
vk::PipelineStageFlagBits2::eColorAttachmentOutput;
AccessState getAccessState(Access access);
@@ -55,9 +55,17 @@ bool writesImage(const AccessState& state);
*/
bool needsBarrier(const AccessState& from, const AccessState& to);
vk::ImageMemoryBarrier makeImageBarrier(vk::Image image,
const vk::ImageSubresourceRange& range,
const AccessState& from,
const AccessState& to);
/**
* A barrier carrying its own stage masks, for recording with
* pipelineBarrier2. Barriers flushed together don't share stages.
*/
vk::ImageMemoryBarrier2 makeImageBarrier(vk::Image image,
const vk::ImageSubresourceRange& range,
const AccessState& from,
const AccessState& to);
void recordImageBarriers(
vk::CommandBuffer commandBuffer,
vk::ArrayProxy<const vk::ImageMemoryBarrier2> barriers);
} // namespace sophia
+82 -5
View File
@@ -57,6 +57,76 @@ void destroyDebugUtilsMessengerEXT(VkInstance instance,
}
}
using DeviceFeatureChain = vk::StructureChain<
vk::PhysicalDeviceFeatures2, vk::PhysicalDeviceVulkan11Features,
vk::PhysicalDeviceVulkan12Features, vk::PhysicalDeviceVulkan13Features>;
// Single source of truth for the device features Sophia requires. Calls
// fn(featureRef, name) for each one, so the same list drives both the
// suitability check and device creation.
template <typename Fn>
static void forEachRequiredFeature(DeviceFeatureChain& chain, Fn&& fn) {
auto& f10 = chain.get<vk::PhysicalDeviceFeatures2>().features;
auto& f11 = chain.get<vk::PhysicalDeviceVulkan11Features>();
auto& f12 = chain.get<vk::PhysicalDeviceVulkan12Features>();
auto& f13 = chain.get<vk::PhysicalDeviceVulkan13Features>();
#define SOPHIA_FEATURE(features, name) fn(features.name, #name)
// Indirect drawing
SOPHIA_FEATURE(f10, multiDrawIndirect);
SOPHIA_FEATURE(f10, drawIndirectFirstInstance);
SOPHIA_FEATURE(f11, shaderDrawParameters);
SOPHIA_FEATURE(f12, drawIndirectCount);
// Bindless descriptors
SOPHIA_FEATURE(f12, descriptorIndexing);
SOPHIA_FEATURE(f12, runtimeDescriptorArray);
SOPHIA_FEATURE(f12, descriptorBindingPartiallyBound);
SOPHIA_FEATURE(f12, descriptorBindingVariableDescriptorCount);
SOPHIA_FEATURE(f12, descriptorBindingUpdateUnusedWhilePending);
SOPHIA_FEATURE(f12, descriptorBindingSampledImageUpdateAfterBind);
SOPHIA_FEATURE(f12, descriptorBindingStorageImageUpdateAfterBind);
SOPHIA_FEATURE(f12, descriptorBindingStorageBufferUpdateAfterBind);
SOPHIA_FEATURE(f12, shaderSampledImageArrayNonUniformIndexing);
SOPHIA_FEATURE(f12, shaderStorageImageArrayNonUniformIndexing);
SOPHIA_FEATURE(f12, shaderStorageBufferArrayNonUniformIndexing);
// Buffer pointers
SOPHIA_FEATURE(f12, bufferDeviceAddress);
SOPHIA_FEATURE(f12, scalarBlockLayout);
// Synchronization
SOPHIA_FEATURE(f12, timelineSemaphore);
SOPHIA_FEATURE(f13, synchronization2);
// Rendering
SOPHIA_FEATURE(f13, dynamicRendering);
#undef SOPHIA_FEATURE
}
static bool checkDeviceFeatureSupport(const vk::PhysicalDevice& device) {
vk::PhysicalDeviceProperties properties = device.getProperties();
if (properties.apiVersion < SOPHIA_VULKAN_API_VERSION) {
log::verbose("Skipping ", properties.deviceName,
": Vulkan 1.3 not supported");
return false;
}
auto supported = device.getFeatures2<
vk::PhysicalDeviceFeatures2, vk::PhysicalDeviceVulkan11Features,
vk::PhysicalDeviceVulkan12Features, vk::PhysicalDeviceVulkan13Features>();
bool allSupported = true;
forEachRequiredFeature(supported, [&](vk::Bool32 has, const char* name) {
if (!has) {
log::verbose("Skipping ", properties.deviceName,
": missing device feature ", name);
allSupported = false;
}
});
return allSupported;
}
Device::Device(Window* window)
: headless{window == nullptr},
enabledExtensions{headless ? std::vector<const char*>{}
@@ -386,6 +456,10 @@ bool Device::isPhysicalDeviceSuitable(const vk::PhysicalDevice& device) {
bool extensionsSupported = checkDeviceExtensionSupport(device);
if (!checkDeviceFeatureSupport(device)) {
return false;
}
if (this->headless) {
return indices.graphicsFamily.has_value() && extensionsSupported;
}
@@ -443,15 +517,17 @@ void Device::createLogicalDevice() {
{vk::DeviceQueueCreateFlags(), queueFamily, 1, &queuePriority});
}
vk::PhysicalDeviceVulkan13Features vulkan13Features{};
vulkan13Features.dynamicRendering = vk::True;
DeviceFeatureChain enabledFeatures;
forEachRequiredFeature(enabledFeatures, [](vk::Bool32& feature, const char*) {
feature = vk::True;
});
auto deviceFeatures = vk::PhysicalDeviceFeatures();
auto createInfo = vk::DeviceCreateInfo(
vk::DeviceCreateFlags(), static_cast<uint32_t>(queueCreateInfos.size()),
queueCreateInfos.data());
createInfo.pEnabledFeatures = &deviceFeatures;
createInfo.pNext = &vulkan13Features;
// Core features are passed through PhysicalDeviceFeatures2 in the pNext
// chain, so pEnabledFeatures must stay null.
createInfo.pNext = &enabledFeatures.get<vk::PhysicalDeviceFeatures2>();
createInfo.enabledExtensionCount =
static_cast<uint32_t>(this->enabledExtensions.size());
@@ -473,6 +549,7 @@ void Device::createLogicalDevice() {
void Device::createAllocator() {
VmaAllocatorCreateInfo createInfo = {};
createInfo.flags = VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT;
createInfo.vulkanApiVersion = SOPHIA_VULKAN_API_VERSION;
createInfo.instance = this->instance.get();
createInfo.physicalDevice = this->physicalDevice;
+2 -3
View File
@@ -56,10 +56,9 @@ void Image::transitionLayout(vk::CommandBuffer commandBuffer,
AccessState to = getStateForLayout(newLayout);
if (needsBarrier(from, to)) {
vk::ImageMemoryBarrier barrier =
vk::ImageMemoryBarrier2 barrier =
makeImageBarrier(this->get(), this->getSubresourceRange(), from, to);
commandBuffer.pipelineBarrier(from.stage, to.stage, {}, nullptr, nullptr,
barrier);
recordImageBarriers(commandBuffer, barrier);
}
this->layout = newLayout;