From b8959f76dbaafa9c123326a1c752976ed48a5752 Mon Sep 17 00:00:00 2001 From: Sascha Willems Date: Sun, 24 Dec 2023 14:50:29 +0100 Subject: [PATCH] Added sample for shader debugprintf --- README.md | 8 +- examples/CMakeLists.txt | 1 + examples/debugprintf/debugprintf.cpp | 210 +++++++++++++++++++++++++ shaders/glsl/debugprintf/toon.frag | 35 +++++ shaders/glsl/debugprintf/toon.frag.spv | Bin 0 -> 3068 bytes shaders/glsl/debugprintf/toon.vert | 37 +++++ shaders/glsl/debugprintf/toon.vert.spv | Bin 0 -> 2928 bytes shaders/hlsl/debugprintf/toon.frag | 34 ++++ shaders/hlsl/debugprintf/toon.frag.spv | Bin 0 -> 1072 bytes shaders/hlsl/debugprintf/toon.vert | 42 +++++ shaders/hlsl/debugprintf/toon.vert.spv | Bin 0 -> 2148 bytes 11 files changed, 366 insertions(+), 1 deletion(-) create mode 100644 examples/debugprintf/debugprintf.cpp create mode 100644 shaders/glsl/debugprintf/toon.frag create mode 100644 shaders/glsl/debugprintf/toon.frag.spv create mode 100644 shaders/glsl/debugprintf/toon.vert create mode 100644 shaders/glsl/debugprintf/toon.vert.spv create mode 100644 shaders/hlsl/debugprintf/toon.frag create mode 100644 shaders/hlsl/debugprintf/toon.frag.spv create mode 100644 shaders/hlsl/debugprintf/toon.vert create mode 100644 shaders/hlsl/debugprintf/toon.vert.spv diff --git a/README.md b/README.md index 55f5c90b..15784101 100644 --- a/README.md +++ b/README.md @@ -423,11 +423,17 @@ Renders a scene to to multiple views (layers) of a single framebuffer to simulat Demonstrates the use of VK_EXT_conditional_rendering to conditionally dispatch render commands based on values from a dedicated buffer. This allows e.g. visibility toggles without having to rebuild command buffers ([blog post](https://www.saschawillems.de/tutorials/vulkan/conditional_rendering)). +#### [Debug shader printf (VK_KHR_shader_non_semantic_info)](examples/debugprintf/) + +Shows how to use printf in a shader to output additional information per invocation. This information can help debugging shader related issues in tools like RenderDoc. + +**Note:** This sample should be run from a graphics debugger like RenderDoc. + #### [Debug utils (VK_EXT_debug_utils)](examples/debugutils/) Shows how to use debug utils for adding labels and colors to Vulkan objects for graphics debuggers. This information helps to identify resources in tools like RenderDoc. -An updated version using ```VK_EXT_debug_utils``` along with an in-depth tutorial is available in the [Official Khronos Vulkan Samples repository](https://github.com/KhronosGroup/Vulkan-Samples/blob/master/samples/extensions/debug_utils). +**Note:** This sample should be run from a graphics debugger like RenderDoc. #### [Negative viewport height (VK_KHR_Maintenance1 or Vulkan 1.1)](examples/negativeviewportheight/) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 2797e6b0..20d8bb53 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -89,6 +89,7 @@ set(EXAMPLES computeshader conditionalrender conservativeraster + debugprintf debugutils deferred deferredmultisampling diff --git a/examples/debugprintf/debugprintf.cpp b/examples/debugprintf/debugprintf.cpp new file mode 100644 index 00000000..77f5140e --- /dev/null +++ b/examples/debugprintf/debugprintf.cpp @@ -0,0 +1,210 @@ +/* +* Vulkan Example - Example for using printf in shaders to help debugging. Can be used in conjunction with a debugging app like RenderDoc (https://renderdoc.org) +* +* See this whitepaper for details: https://www.lunarg.com/wp-content/uploads/2021/08/Using-Debug-Printf-02August2021.pdf +* +* Copyright (C) 2023 by Sascha Willems - www.saschawillems.de +* +* This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT) +*/ + +/* +* The only change required for printf in shaders on the application is enabling the VK_KHR_shader_non_semantic_info extensions +* The actual printing is done in the shaders (see toon.vert from the glsl/hlsl) folder +* For glsl shaders that use this feature, the GL_EXT_debug_printf extension needs to be enabled +*/ + +#include "vulkanexamplebase.h" +#include "VulkanglTFModel.h" + +#define ENABLE_VALIDATION false + +class VulkanExample : public VulkanExampleBase +{ +public: + vks::Buffer uniformBuffer; + vkglTF::Model scene; + + struct UBOVS { + glm::mat4 projection; + glm::mat4 model; + glm::vec4 lightPos = glm::vec4(0.0f, 5.0f, 15.0f, 1.0f); + } uboVS; + + VkPipelineLayout pipelineLayout{ VK_NULL_HANDLE }; + VkPipeline pipeline{ VK_NULL_HANDLE }; + VkDescriptorSetLayout descriptorSetLayout{ VK_NULL_HANDLE }; + VkDescriptorSet descriptorSet{ VK_NULL_HANDLE }; + + VulkanExample() : VulkanExampleBase(ENABLE_VALIDATION) + { + title = "Debug output with shader printf"; + camera.setRotation(glm::vec3(-4.35f, 16.25f, 0.0f)); + camera.setRotationSpeed(0.5f); + camera.setPosition(glm::vec3(0.1f, 1.1f, -8.5f)); + camera.setPerspective(60.0f, (float)width / (float)height, 0.1f, 256.0f); + + // Using printf requires the non semantic info extension to be enabled + enabledDeviceExtensions.push_back(VK_KHR_SHADER_NON_SEMANTIC_INFO_EXTENSION_NAME); + } + + ~VulkanExample() + { + // Note : Inherited destructor cleans up resources stored in base class + vkDestroyPipeline(device, pipeline, nullptr); + vkDestroyPipelineLayout(device, pipelineLayout, nullptr); + vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr); + uniformBuffer.destroy(); + } + + void loadAssets() + { + scene.loadFromFile(getAssetPath() + "models/treasure_smooth.gltf", vulkanDevice, queue, vkglTF::FileLoadingFlags::PreTransformVertices | vkglTF::FileLoadingFlags::PreMultiplyVertexColors | vkglTF::FileLoadingFlags::FlipY); + } + + void buildCommandBuffers() + { + VkCommandBufferBeginInfo cmdBufInfo = vks::initializers::commandBufferBeginInfo(); + VkClearValue clearValues[2]; + + for (int32_t i = 0; i < drawCmdBuffers.size(); ++i) + { + VK_CHECK_RESULT(vkBeginCommandBuffer(drawCmdBuffers[i], &cmdBufInfo)); + + clearValues[0].color = defaultClearColor; + clearValues[1].depthStencil = { 1.0f, 0 }; + + VkRenderPassBeginInfo renderPassBeginInfo = vks::initializers::renderPassBeginInfo(); + renderPassBeginInfo.renderPass = renderPass; + renderPassBeginInfo.framebuffer = frameBuffers[i]; + renderPassBeginInfo.renderArea.extent.width = width; + renderPassBeginInfo.renderArea.extent.height = height; + renderPassBeginInfo.clearValueCount = 2; + renderPassBeginInfo.pClearValues = clearValues; + + vkCmdBeginRenderPass(drawCmdBuffers[i], &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE); + VkViewport viewport = vks::initializers::viewport((float)width, (float)height, 0.0f, 1.0f); + vkCmdSetViewport(drawCmdBuffers[i], 0, 1, &viewport); + VkRect2D scissor = vks::initializers::rect2D(width, height, 0, 0); + vkCmdSetScissor(drawCmdBuffers[i], 0, 1, &scissor); + vkCmdBindDescriptorSets(drawCmdBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSet, 0, nullptr); + vkCmdBindPipeline(drawCmdBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + scene.draw(drawCmdBuffers[i]); + drawUI(drawCmdBuffers[i]); + vkCmdEndRenderPass(drawCmdBuffers[i]); + VK_CHECK_RESULT(vkEndCommandBuffer(drawCmdBuffers[i])); + } + } + + void setupDescriptors() + { + // Pool + std::vector poolSizes = { + vks::initializers::descriptorPoolSize(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1), + }; + VkDescriptorPoolCreateInfo descriptorPoolInfo = vks::initializers::descriptorPoolCreateInfo(poolSizes, 1); + VK_CHECK_RESULT(vkCreateDescriptorPool(device, &descriptorPoolInfo, nullptr, &descriptorPool)); + + // Layout + std::vector setLayoutBindings = { + vks::initializers::descriptorSetLayoutBinding(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT, 0), + }; + VkDescriptorSetLayoutCreateInfo descriptorLayout = vks::initializers::descriptorSetLayoutCreateInfo(setLayoutBindings); + VK_CHECK_RESULT(vkCreateDescriptorSetLayout(device, &descriptorLayout, nullptr, &descriptorSetLayout)); + + // Set + VkDescriptorSetAllocateInfo allocInfo = vks::initializers::descriptorSetAllocateInfo(descriptorPool, &descriptorSetLayout, 1); + VK_CHECK_RESULT(vkAllocateDescriptorSets(device, &allocInfo, &descriptorSet)); + std::vector writeDescriptorSets = { + vks::initializers::writeDescriptorSet(descriptorSet, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 0, &uniformBuffer.descriptor), + }; + vkUpdateDescriptorSets(device, static_cast(writeDescriptorSets.size()), writeDescriptorSets.data(), 0, nullptr); + } + + void preparePipelines() + { + // Layout + VkPipelineLayoutCreateInfo pipelineLayoutCreateInfo = vks::initializers::pipelineLayoutCreateInfo(&descriptorSetLayout, 1); + VK_CHECK_RESULT(vkCreatePipelineLayout(device, &pipelineLayoutCreateInfo, nullptr, &pipelineLayout)); + + // Pipeline + VkPipelineInputAssemblyStateCreateInfo inputAssemblyStateCI = vks::initializers::pipelineInputAssemblyStateCreateInfo(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, 0, VK_FALSE); + VkPipelineRasterizationStateCreateInfo rasterizationStateCI = vks::initializers::pipelineRasterizationStateCreateInfo(VK_POLYGON_MODE_FILL, VK_CULL_MODE_BACK_BIT, VK_FRONT_FACE_COUNTER_CLOCKWISE, 0); + VkPipelineColorBlendAttachmentState blendAttachmentState = vks::initializers::pipelineColorBlendAttachmentState(0xf, VK_FALSE); + VkPipelineColorBlendStateCreateInfo colorBlendStateCI = vks::initializers::pipelineColorBlendStateCreateInfo(1, &blendAttachmentState); + VkPipelineDepthStencilStateCreateInfo depthStencilStateCI = vks::initializers::pipelineDepthStencilStateCreateInfo(VK_TRUE, VK_TRUE, VK_COMPARE_OP_LESS_OR_EQUAL); + VkPipelineViewportStateCreateInfo viewportStateCI = vks::initializers::pipelineViewportStateCreateInfo(1, 1, 0); + VkPipelineMultisampleStateCreateInfo multisampleStateCI = vks::initializers::pipelineMultisampleStateCreateInfo(VK_SAMPLE_COUNT_1_BIT, 0); + std::vector dynamicStateEnables = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }; + VkPipelineDynamicStateCreateInfo dynamicStateCI = vks::initializers::pipelineDynamicStateCreateInfo(dynamicStateEnables); + std::array shaderStages; + + VkGraphicsPipelineCreateInfo pipelineCI = vks::initializers::pipelineCreateInfo(pipelineLayout, renderPass); + pipelineCI.pInputAssemblyState = &inputAssemblyStateCI; + pipelineCI.pRasterizationState = &rasterizationStateCI; + pipelineCI.pColorBlendState = &colorBlendStateCI; + pipelineCI.pMultisampleState = &multisampleStateCI; + pipelineCI.pViewportState = &viewportStateCI; + pipelineCI.pDepthStencilState = &depthStencilStateCI; + pipelineCI.pDynamicState = &dynamicStateCI; + pipelineCI.stageCount = static_cast(shaderStages.size()); + pipelineCI.pStages = shaderStages.data(); + pipelineCI.pVertexInputState = vkglTF::Vertex::getPipelineVertexInputState({vkglTF::VertexComponent::Position, vkglTF::VertexComponent::Normal, vkglTF::VertexComponent::Color}); + + // Toon shading pipeline + shaderStages[0] = loadShader(getShadersPath() + "debugprintf/toon.vert.spv", VK_SHADER_STAGE_VERTEX_BIT); + shaderStages[1] = loadShader(getShadersPath() + "debugprintf/toon.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT); + VK_CHECK_RESULT(vkCreateGraphicsPipelines(device, pipelineCache, 1, &pipelineCI, nullptr, &pipeline)); + } + + // Prepare and initialize uniform buffer containing shader uniforms + void prepareUniformBuffers() + { + VK_CHECK_RESULT(vulkanDevice->createBuffer(VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, &uniformBuffer, sizeof(uboVS))); + VK_CHECK_RESULT(uniformBuffer.map()); + } + + void updateUniformBuffers() + { + uboVS.projection = camera.matrices.perspective; + uboVS.model = camera.matrices.view; + memcpy(uniformBuffer.mapped, &uboVS, sizeof(uboVS)); + } + + void draw() + { + VulkanExampleBase::prepareFrame(); + submitInfo.commandBufferCount = 1; + submitInfo.pCommandBuffers = &drawCmdBuffers[currentBuffer]; + VK_CHECK_RESULT(vkQueueSubmit(queue, 1, &submitInfo, VK_NULL_HANDLE)); + VulkanExampleBase::submitFrame(); + } + + void prepare() + { + VulkanExampleBase::prepare(); + loadAssets(); + prepareUniformBuffers(); + setupDescriptors(); + preparePipelines(); + buildCommandBuffers(); + prepared = true; + } + + virtual void render() + { + if (!prepared) + return; + updateUniformBuffers(); + draw(); + } + + virtual void OnUpdateUIOverlay(vks::UIOverlay *overlay) + { + if (overlay->header("Info")) { + overlay->text("Please run this sample with a graphics debugger attached"); + } + } +}; + +VULKAN_EXAMPLE_MAIN() diff --git a/shaders/glsl/debugprintf/toon.frag b/shaders/glsl/debugprintf/toon.frag new file mode 100644 index 00000000..b947da36 --- /dev/null +++ b/shaders/glsl/debugprintf/toon.frag @@ -0,0 +1,35 @@ +#version 450 + +layout (binding = 1) uniform sampler2D samplerColorMap; + +layout (location = 0) in vec3 inNormal; +layout (location = 1) in vec3 inColor; +layout (location = 2) in vec3 inViewVec; +layout (location = 3) in vec3 inLightVec; + +layout (location = 0) out vec4 outFragColor; + +void main() +{ + // Desaturate color + vec3 color = vec3(mix(inColor, vec3(dot(vec3(0.2126,0.7152,0.0722), inColor)), 0.65)); + + // High ambient colors because mesh materials are pretty dark + vec3 ambient = color * vec3(1.0); + vec3 N = normalize(inNormal); + vec3 L = normalize(inLightVec); + vec3 V = normalize(inViewVec); + vec3 R = reflect(-L, N); + vec3 diffuse = max(dot(N, L), 0.0) * color; + vec3 specular = pow(max(dot(R, V), 0.0), 16.0) * vec3(0.75); + outFragColor = vec4(ambient + diffuse * 1.75 + specular, 1.0); + + float intensity = dot(N,L); + float shade = 1.0; + shade = intensity < 0.5 ? 0.75 : shade; + shade = intensity < 0.35 ? 0.6 : shade; + shade = intensity < 0.25 ? 0.5 : shade; + shade = intensity < 0.1 ? 0.25 : shade; + + outFragColor.rgb = inColor * 3.0 * shade; +} \ No newline at end of file diff --git a/shaders/glsl/debugprintf/toon.frag.spv b/shaders/glsl/debugprintf/toon.frag.spv new file mode 100644 index 0000000000000000000000000000000000000000..e28490d3dfff29b0a8cedae779f4dc28c84235bd GIT binary patch literal 3068 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 literal 0 HcmV?d00001 diff --git a/shaders/hlsl/debugprintf/toon.frag b/shaders/hlsl/debugprintf/toon.frag new file mode 100644 index 00000000..ef1637df --- /dev/null +++ b/shaders/hlsl/debugprintf/toon.frag @@ -0,0 +1,34 @@ +Texture2D textureColorMap : register(t1); +SamplerState samplerColorMap : register(s1); + +struct VSOutput +{ +[[vk::location(0)]] float3 Normal : NORMAL0; +[[vk::location(1)]] float3 Color : COLOR0; +[[vk::location(2)]] float3 ViewVec : TEXCOORD1; +[[vk::location(3)]] float3 LightVec : TEXCOORD2; +}; + +float4 main(VSOutput input) : SV_TARGET +{ + // Desaturate color + float3 color = float3(lerp(input.Color, dot(float3(0.2126,0.7152,0.0722), input.Color).xxx, 0.65)); + + // High ambient colors because mesh materials are pretty dark + float3 ambient = color * float3(1.0, 1.0, 1.0); + float3 N = normalize(input.Normal); + float3 L = normalize(input.LightVec); + float3 V = normalize(input.ViewVec); + float3 R = reflect(-L, N); + float3 diffuse = max(dot(N, L), 0.0) * color; + float3 specular = pow(max(dot(R, V), 0.0), 16.0) * float3(0.75, 0.75, 0.75); + + float intensity = dot(N,L); + float shade = 1.0; + shade = intensity < 0.5 ? 0.75 : shade; + shade = intensity < 0.35 ? 0.6 : shade; + shade = intensity < 0.25 ? 0.5 : shade; + shade = intensity < 0.1 ? 0.25 : shade; + + return float4(input.Color * 3.0 * shade, 1); +} \ No newline at end of file diff --git a/shaders/hlsl/debugprintf/toon.frag.spv b/shaders/hlsl/debugprintf/toon.frag.spv new file mode 100644 index 0000000000000000000000000000000000000000..630d3ba4cea0c94dfc991299ea4a62666604cbc1 GIT binary patch literal 1072 zcmY+C$xfS5424ZX7z$)yo>~F{N(+iACLtjp2%>C4{}Lj^4h6Ac0hK82nkVRr4ZH#C zZu$`LK5URU-{p4%zvS5G_}ISpI>|(#-K@pT(zc*qzMLfj(`9YJSE>iq;;*aI;_}Kj z@@Y%rLM~&S)~YxmJ8zs_z>|eb$=YODG2}H!Q8Mr+B2vntJ-aCWXFxSKvD_su8&a-A{`GEV^TNcPGPCO~#~%VNHE~zK*2LYyOH2PYUEd>o zcRSL|r-S~wna>D6K0Iu=J{$6-QeN|Oin#~p;(WdGoQXSmFV-nTrt82>2^8dwgFZKw}4(`>Lx0#T^vnO`v+Pq@4?8mi}it{pd zM4fA=6r*KVt}Q5rXBKs?omPyNJ-T*AF+BCu{ndH7^Q??_VaLvWj(KYK0?#2dPTBBID014Lxj1&wbTzy-&^41tBjCrSJR{#;*7 zJYVmWPBu0vx=!8tbXCvh8i!k)YjDnWxF!8(b-6}wX>+Y^v-q+6cC}QFlem0RIoyt` z(QY}4_Y%cjl%LFvB;HJ2h2_f5>3;DzimQ97n4_GJ>2#O8-l+eF+ffX_+a$?Lnk6El ziq2h-w0YdDGn^|XCs8#@;$S8iIh)v1Ue4$2s90Wq?TV>>JE{~;cc7(OP2Pr%DX(}= z|F9U!*PJqO?&vuATG^#CIeBmAK8MNY%7J!^h4VqQ|D`G-QhmJ+?X}u1{vJ_WINLrh z6vNH6t+jAttQH3o=ThHBSbDdxE>GT{_Se1?u7@QOu+xL&wCaml0(Sahc1y@BnqHZG z>&>U7Ff1*P`&uo&hxNXv-q)lF|C_^#Skixb%>px<0gq~!dw}T=er5)y4jA6qc=%@H z;hBwRzF^KFpMDNWScV5{(5>pU+Us#{_&+wzzARZ)9gBmn<-wV}`8r#D9&y<`?nd6Z zgj&2amT_1Sdu+t@O7GMjNcvO-K49vByHcFf-XnZPdm!P=(G+*r;j6;bHh+(F7MR+- z9-Yti3-?LqOwJof`-RQy11VSf6DX=*JGJ2tzNQ^6@C0Ai4p(Y|Z=@K1w=!>PA5_FG z?dZhVd^r%BY#B8Ifjk^@p5&Hc|LO${Mc{@K<7vEzEe$F#n zA+^J`T{Y;<6A9-a{8o_1nba7Qkk1PBYem1tCD;)bd2q5Eev8P#ZaL4SV@Fu^ITNyx zLmkU`E*(37#8|yo(y=2ftH+E3 z;c1D@nD3ey)3?{D&+5#iHtZ~`V`tCGMojkXIbrMw%W|ydyll)0VVVDjX2+ZsB-EIf od{5u{Vv1+gHNG+413KRqcSUd2&YMT)yCMgjZ;G0~b<&jN4^$V2CjbBd literal 0 HcmV?d00001