Crafter.Graphics/examples/VulkanTriangle/main.cpp

279 lines
11 KiB
C++
Raw Normal View History

2026-07-22 18:09:06 +02:00
//SPDX-License-Identifier: MIT
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
2026-05-18 18:43:30 +02:00
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
2026-03-02 23:53:13 +01:00
#include "vulkan/vulkan.h"
2026-05-18 18:43:30 +02:00
#endif
2026-04-05 22:53:59 +02:00
#include <cassert>
2025-06-13 23:59:36 +02:00
import Crafter.Graphics;
using namespace Crafter;
2026-01-28 01:06:48 +01:00
import std;
2026-01-29 19:46:53 +01:00
import Crafter.Event;
2026-01-29 20:35:55 +01:00
import Crafter.Math;
2025-06-13 23:59:36 +02:00
2026-05-18 18:43:30 +02:00
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
2025-06-13 23:59:36 +02:00
int main() {
feat(device): applications ask for the device features their shaders use Device - Device::Initialize(required, optional) takes DeviceFeatures, the five core feature structs vulkan10 to vulkan14. The library enables only what its own shaders and code use. Device::enabledFeatures says what was enabled, supportedFeatures what the GPU has, apiVersion the version in use. - shaderInt16, shaderFloat16, storageBuffer16BitAccess, scalarBlockLayout and the uniform-buffer, storage-buffer and storage-image non-uniform indexing bits are no longer enabled for everyone. An application whose shaders take uint16_t spec constants passes {.vulkan10 = {.shaderInt16 = VK_TRUE}}. scalarBlockLayout stays on with the NVIDIA workaround, whose TLAS address may sit in a struct's tail padding. - Every GPU is weighed before one is chosen: Vulkan 1.3, the swapchain and, below 1.4, maintenance5 extensions, the library's needs, the required features. One that can't run the application is skipped, saying why when it ranks higher. A CPU device runs it only when nothing else is listed, and CRAFTER_GRAPHICS_DEVICE never falls back. A GPU two drivers both list is tried through each. - When no GPU qualifies, Initialize throws naming each GPU's gaps. It destroys the instance it made, before any Wayland connection, so it can be called again asking for less. - maintenance5 is vulkan14's on a 1.4 GPU and VK_KHR_maintenance5's below. CRAFTER_GRAPHICS_NO_VULKAN_1_4 runs as a 1.3 application, to exercise that path on a 1.4 GPU. - VK_KHR_spirv_1_4 and VK_KHR_shader_float_controls, core since 1.2, are no longer enabled, nor VK_EXT_shader_atomic_float, which was enabled without its feature struct. memoryDecompression's feature bit is queried before it is enabled. Shaders - VulkanShader checks a module's capabilities, declared directly or implicitly, against the enabled core features, the GPU's subgroup operations and the optional groups, and throws naming the field to pass, or saying the GPU lacks it. The tables are generated from vk.xml and SPIR-V's grammar. - WorkaroundNvidiaAS::Patch sized a row-major matrix push member by its columns, so the TLAS address could land inside it. Window - The swapchain format is one whose own feature flags allow storage reads and writes without a format, which since Vulkan 1.3 each format says for itself; shaderStorageImageRead/WriteWithoutFormat are no longer required. Tests - DeviceFeatures: ResolveFeatures, ChooseDevice and CheckShaderCapabilities, without a GPU. - FeatureRequests: required and optional features end to end, the 1.3 path, and a retry after a refusal. - PushConstantRewrite: a row-major mat3x4 push member. - HeapASRead, HeapDynamicIndexHit, RTPassPushData and the ray-tracing examples ask for the features their shaders use. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 06:40:02 +01:00
// The raygen takes the heap's buffer start as a uint16_t spec constant.
Device::Initialize({.vulkan10 = {.shaderInt16 = VK_TRUE}});
2026-03-09 20:10:19 +01:00
Window window(1280, 720, "HelloVulkan");
2026-01-31 21:08:42 +01:00
VkCommandBuffer cmd = window.StartInit();
2026-04-05 22:53:59 +02:00
DescriptorHeapVulkan descriptorHeap;
descriptorHeap.Initialize(1,1,0);
2026-01-31 21:08:42 +01:00
2026-04-05 22:53:59 +02:00
VkSpecializationMapEntry entry = {
.constantID = 0,
.offset = 0,
.size = sizeof(uint16_t)
};
2026-02-03 21:03:11 +01:00
2026-04-05 22:53:59 +02:00
VkSpecializationInfo specilizationInfo = {
.mapEntryCount = 1,
.pMapEntries = &entry,
.dataSize = sizeof(uint16_t),
.pData = &descriptorHeap.bufferStartElement
};
2026-02-03 21:03:11 +01:00
2026-04-05 22:53:59 +02:00
std::array<VulkanShader, 3> shaders{{
{"raygen.spv", "main", VK_SHADER_STAGE_RAYGEN_BIT_KHR, &specilizationInfo},
{"miss.spv", "main", VK_SHADER_STAGE_MISS_BIT_KHR, nullptr},
{"closesthit.spv", "main", VK_SHADER_STAGE_CLOSEST_HIT_BIT_KHR, nullptr}
}};
2026-02-03 21:03:11 +01:00
2026-04-05 22:53:59 +02:00
ShaderBindingTableVulkan shaderTable;
shaderTable.Init(shaders);
std::array<VkRayTracingShaderGroupCreateInfoKHR, 1> raygenGroups {{
{
.sType = VK_STRUCTURE_TYPE_RAY_TRACING_SHADER_GROUP_CREATE_INFO_KHR,
.type = VK_RAY_TRACING_SHADER_GROUP_TYPE_GENERAL_KHR,
.generalShader = 0,
.closestHitShader = VK_SHADER_UNUSED_KHR,
.anyHitShader = VK_SHADER_UNUSED_KHR,
.intersectionShader = VK_SHADER_UNUSED_KHR,
},
}};
std::array<VkRayTracingShaderGroupCreateInfoKHR, 1> missGroups {{
{
.sType = VK_STRUCTURE_TYPE_RAY_TRACING_SHADER_GROUP_CREATE_INFO_KHR,
.type = VK_RAY_TRACING_SHADER_GROUP_TYPE_GENERAL_KHR,
.generalShader = 1,
.closestHitShader = VK_SHADER_UNUSED_KHR,
.anyHitShader = VK_SHADER_UNUSED_KHR,
.intersectionShader = VK_SHADER_UNUSED_KHR,
},
}};
std::array<VkRayTracingShaderGroupCreateInfoKHR, 1> hitGroups {{
{
.sType = VK_STRUCTURE_TYPE_RAY_TRACING_SHADER_GROUP_CREATE_INFO_KHR,
.type = VK_RAY_TRACING_SHADER_GROUP_TYPE_TRIANGLES_HIT_GROUP_KHR,
.generalShader = VK_SHADER_UNUSED_KHR,
.closestHitShader = 2,
.anyHitShader = VK_SHADER_UNUSED_KHR,
.intersectionShader = VK_SHADER_UNUSED_KHR,
},
}};
PipelineRTVulkan pipeline;
pipeline.Init(cmd, raygenGroups, missGroups, hitGroups, shaderTable);
2025-06-13 23:59:36 +02:00
2026-01-28 18:51:11 +01:00
Mesh triangleMesh;
2026-02-24 02:32:37 +01:00
std::array<Vector<float, 3, 3>, 3> verts {{{-150, -150, 100}, {0, 150, 100}, {150, -150, 100}}};
2026-01-29 19:18:47 +01:00
std::array<std::uint32_t, 3> index {{2,1,0}};
2026-01-28 18:51:11 +01:00
triangleMesh.Build(verts, index, cmd);
2026-02-24 02:32:37 +01:00
2026-03-09 20:10:19 +01:00
RenderingElement3D renderer = {
2026-02-24 02:32:37 +01:00
.instance = {
.instanceCustomIndex = 0,
.mask = 0xFF,
.instanceShaderBindingTableRecordOffset = 0,
.flags = VK_GEOMETRY_INSTANCE_FORCE_OPAQUE_BIT_KHR,
.accelerationStructureReference = triangleMesh.blasAddr
}
};
2026-03-09 20:10:19 +01:00
RenderingElement3D::elements.emplace_back(&renderer);
2026-02-24 02:32:37 +01:00
2026-01-29 20:35:55 +01:00
MatrixRowMajor<float, 4, 3, 1> transform = MatrixRowMajor<float, 4, 3, 1>::Identity();
2026-05-18 18:43:30 +02:00
transform.Store(reinterpret_cast<float*>(renderer.instance.transform.matrix));
2026-03-09 20:10:19 +01:00
RenderingElement3D::BuildTLAS(cmd, 0);
2026-04-05 22:53:59 +02:00
RenderingElement3D::BuildTLAS(cmd, 1);
RenderingElement3D::BuildTLAS(cmd, 2);
window.FinishInit();
2026-05-01 23:35:37 +02:00
auto imgSlots = descriptorHeap.AllocateImageSlots(1);
auto bufSlots = descriptorHeap.AllocateBufferSlots(1);
2026-04-05 22:53:59 +02:00
VkDeviceAddressRangeKHR tlasRange0 = {
.address = RenderingElement3D::tlases[0].address,
};
2025-06-13 23:59:36 +02:00
2026-04-05 22:53:59 +02:00
VkDeviceAddressRangeKHR tlasRange1 = {
.address = RenderingElement3D::tlases[1].address,
2026-01-29 02:05:18 +01:00
};
2026-04-05 22:53:59 +02:00
VkDeviceAddressRangeKHR tlasRange2 = {
.address = RenderingElement3D::tlases[2].address,
2026-01-29 02:05:18 +01:00
};
2026-04-05 22:53:59 +02:00
VkImageDescriptorInfoEXT imageInfo0 = {
.sType = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT,
.pView = &window.imageViews[0],
.layout = VK_IMAGE_LAYOUT_GENERAL
};
VkImageDescriptorInfoEXT imageInfo1 = {
.sType = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT,
.pView = &window.imageViews[1],
.layout = VK_IMAGE_LAYOUT_GENERAL
};
2026-02-24 02:32:37 +01:00
2026-04-05 22:53:59 +02:00
VkImageDescriptorInfoEXT imageInfo2 = {
.sType = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT,
.pView = &window.imageViews[2],
.layout = VK_IMAGE_LAYOUT_GENERAL
};
VkResourceDescriptorInfoEXT resources[6] = {
2026-01-29 02:05:18 +01:00
{
2026-04-05 22:53:59 +02:00
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
.type = VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR,
.data = { .pAddressRange = &tlasRange0}
2026-01-29 02:05:18 +01:00
},
{
2026-04-05 22:53:59 +02:00
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
.type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
.data = { .pImage = &imageInfo0 }
2026-02-24 02:32:37 +01:00
},
{
2026-04-05 22:53:59 +02:00
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
.type = VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR,
.data = { .pAddressRange = &tlasRange1}
},
{
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
.type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
.data = { .pImage = &imageInfo1 }
},
{
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
.type = VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR,
.data = { .pAddressRange = &tlasRange2}
},
{
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
.type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
.data = { .pImage = &imageInfo2 }
},
2026-01-29 02:05:18 +01:00
};
2026-02-24 02:32:37 +01:00
2026-04-05 22:53:59 +02:00
VkHostAddressRangeEXT destinations[6] = {
{
2026-05-01 23:35:37 +02:00
.address = descriptorHeap.resourceHeap[0].value + descriptorHeap.BufferByteOffset(bufSlots.firstElement),
2026-04-05 22:53:59 +02:00
.size = Device::descriptorHeapProperties.bufferDescriptorSize
},
{
2026-05-01 23:35:37 +02:00
.address = descriptorHeap.resourceHeap[0].value + descriptorHeap.ImageByteOffset(imgSlots.firstElement),
2026-04-05 22:53:59 +02:00
.size = Device::descriptorHeapProperties.imageDescriptorSize
},
{
2026-05-01 23:35:37 +02:00
.address = descriptorHeap.resourceHeap[1].value + descriptorHeap.BufferByteOffset(bufSlots.firstElement),
2026-04-05 22:53:59 +02:00
.size = Device::descriptorHeapProperties.bufferDescriptorSize
},
{
2026-05-01 23:35:37 +02:00
.address = descriptorHeap.resourceHeap[1].value + descriptorHeap.ImageByteOffset(imgSlots.firstElement),
2026-04-05 22:53:59 +02:00
.size = Device::descriptorHeapProperties.imageDescriptorSize
},
{
2026-05-01 23:35:37 +02:00
.address = descriptorHeap.resourceHeap[2].value + descriptorHeap.BufferByteOffset(bufSlots.firstElement),
2026-04-05 22:53:59 +02:00
.size = Device::descriptorHeapProperties.bufferDescriptorSize
},
{
2026-05-01 23:35:37 +02:00
.address = descriptorHeap.resourceHeap[2].value + descriptorHeap.ImageByteOffset(imgSlots.firstElement),
2026-04-05 22:53:59 +02:00
.size = Device::descriptorHeapProperties.imageDescriptorSize
},
};
2026-01-29 02:05:18 +01:00
2026-04-05 22:53:59 +02:00
Device::vkWriteResourceDescriptorsEXT(Device::device, 6, resources, destinations);
descriptorHeap.resourceHeap[0].FlushDevice();
descriptorHeap.resourceHeap[1].FlushDevice();
descriptorHeap.resourceHeap[2].FlushDevice();
window.descriptorHeap = &descriptorHeap;
2026-05-01 23:35:37 +02:00
RTPass rtPass(&pipeline);
window.passes.push_back(&rtPass);
2026-01-29 01:31:17 +01:00
fix(vulkan-rt): work around NVIDIA descriptor-heap AS-read device-loss (#15) Reading an acceleration structure through VK_EXT_descriptor_heap aborts with VK_ERROR_DEVICE_LOST on NVIDIA 610.43.02 — a brand-new-extension driver fault isolated in #7 (engine setup is correct and validation-clean; images/buffers through the same heap work, and both traceRayEXT and inline rayQuery fault identically on the AS read). An acceleration structure can equally be reached by its device address via OpConvertUToAccelerationStructureKHR, which reads no descriptor and so never touches the faulting heap path. glslang has no GLSL spelling for that conversion, so VulkanShader rewrites the compiled SPIR-V at module-load time: every `OpLoad %accelStruct <heap-ptr>` becomes a load of the TLAS device address from a synthesized push-constant block followed by the convert. RTPass pushes the active frame's TLAS address into that push constant. User GLSL and example code are unchanged; acceleration structures still bind into the heap normally. The workaround is gated on Device::workaroundDescriptorHeapAS (true only on the NVIDIA proprietary driver) and confined to one fenced block in Crafter.Graphics-ShaderVulkan.cppm plus the RTPass push and the shaderInt64 feature toggle — delete those once a fixed NVIDIA driver ships and the heap AS read becomes the direct path again. Verified: VulkanTriangle ray-traces correctly on native NVIDIA (RTX 4090), validation-layer-clean, no device loss. The SPIR-V rewrite was independently validated with spirv-val on both the VulkanTriangle and Sponza raygen modules. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 01:59:54 +00:00
// NOTE: reading the acceleration structure through VK_EXT_descriptor_heap
// aborts with VK_ERROR_DEVICE_LOST on NVIDIA 610.43.02 (a driver fault —
// see #7). The engine transparently works around it: on the NVIDIA driver
// VulkanShader rewrites the heap AS read into a TLAS-device-address +
// OpConvertUToAccelerationStructureKHR path and RTPass feeds the address in
// as push data. Nothing here (or in raygen.glsl) changes. See README.md
// ("Native status") and interfaces/Crafter.Graphics-ShaderVulkan.cppm.
2026-01-28 01:06:48 +01:00
window.Render();
2025-06-13 23:59:36 +02:00
window.StartSync();
}
2026-05-18 18:43:30 +02:00
#else
// DOM-mode port. Same scene (one triangle), software-emulated raytracing
// via compute. Shaders are read from .wgsl files shipped as static
// assets (see project.cpp). Renders barycentric colors via the
// hit/miss/raygen mega-switch in PipelineRTWebGPU.
int main() {
Device::Initialize();
static Window window(1280, 720, "HelloVulkan");
auto cmd = window.StartInit();
DescriptorHeapWebGPU heap;
heap.Initialize(/*images*/ 4, /*buffers*/ 4, /*samplers*/ 2);
std::array<WebGPUShader, 3> shaders {{
WebGPUShader(std::filesystem::path("raygen.wgsl"), "raygen_main", WebGPURTStage::Raygen),
WebGPUShader(std::filesystem::path("miss.wgsl"), "miss_main", WebGPURTStage::Miss),
WebGPUShader(std::filesystem::path("closesthit.wgsl"), "closesthit_main", WebGPURTStage::ClosestHit),
}};
ShaderBindingTableWebGPU sbt;
sbt.Init(shaders);
std::array<RTShaderGroup, 1> raygenGroups {{
{ .type = RTShaderGroupType::General, .generalShader = 0 },
}};
std::array<RTShaderGroup, 1> missGroups {{
{ .type = RTShaderGroupType::General, .generalShader = 1 },
}};
std::array<RTShaderGroup, 1> hitGroups {{
{ .type = RTShaderGroupType::TrianglesHitGroup, .closestHitShader = 2 },
}};
PipelineRTWebGPU pipeline;
pipeline.Init(cmd, raygenGroups, missGroups, hitGroups, sbt);
Mesh triangleMesh;
std::array<Vector<float, 3, 3>, 3> verts {{{-150, -150, 100}, {0, 150, 100}, {150, -150, 100}}};
std::array<std::uint32_t, 3> index {{2, 1, 0}};
triangleMesh.Build(verts, index, cmd);
static RenderingElement3D renderer;
renderer.instance.transform.matrix[0][0] = 1; renderer.instance.transform.matrix[0][1] = 0; renderer.instance.transform.matrix[0][2] = 0; renderer.instance.transform.matrix[0][3] = 0;
renderer.instance.transform.matrix[1][0] = 0; renderer.instance.transform.matrix[1][1] = 1; renderer.instance.transform.matrix[1][2] = 0; renderer.instance.transform.matrix[1][3] = 0;
renderer.instance.transform.matrix[2][0] = 0; renderer.instance.transform.matrix[2][1] = 0; renderer.instance.transform.matrix[2][2] = 1; renderer.instance.transform.matrix[2][3] = 0;
renderer.instance.instanceCustomIndex = 0;
renderer.instance.mask = 0xFF;
renderer.instance.instanceShaderBindingTableRecordOffset = 0;
renderer.instance.flags = kRTGeometryInstanceForceOpaque;
renderer.instance.accelerationStructureReference = triangleMesh.blasAddr;
RenderingElement3D::Add(&renderer);
RenderingElement3D::BuildTLAS(cmd, 0);
window.descriptorHeap = &heap;
window.FinishInit();
RTPass rtPass(&pipeline);
window.passes.push_back(&rtPass);
window.Render();
window.StartUpdate();
window.StartSync();
}
#endif