Vulkan based Ab4d.SharpEngine on Raspberry Pi 4 with a touch screen

by Rok Mandeljc; February 2022 (updated January 2023)

 

The improved hardware capabilities of the Raspberry Pi 4 and recent advances of the open-source V3DV Vulkan driver for Raspberry Pi make it possible to run Vulkan applications on your Pi. As shown in this post, this requires relatively little effort when using the latest version of the Raspberry Pi OS.

This post comes in two parts: the first part assumes that your Raspberry Pi 4 is connected to a standard external PC monitor, and describes the standard steps required to set up Vulkan, dotnet, and to get SharpEngine demo running. In the second part, an attached LCD screen (a Raspberry Pi add-on) is used instead. The post deals with caveats and issues that come with a particular LCD screen, although the information might be applicable to wider range of Raspberry Pi LCD screens out in the wild.

 

Part 1: using an external PC monitor

Depending on your monitor and luck, things might work out of the box. If not, you might need to fiddle with HDMI settings on the Pi to get the display working. But overall, using a PC monitor should work with the default v3d display driver for Raspberry Pi, which is what we need for working 3D Vulkan acceleration.

Install the OS

Instead of the “classic” 32-bit Raspbian OS, we will be using 64-bit variant that has just recently become official. The instructions should largely remain unchanged for 32-bit Raspbian OS, but given that Vulkan works only with Raspberry Pi 4, using 64-bit OS should be preferable to 32-bit in most cases (and comes with substantial performance improvements).

Go to https://www.raspberrypi.com/software/operating-systems to download the image, and choose “Raspberry Pi OS with desktop” under “Raspberry Pi OS (64-bit)”. Note that the first entry is regular 32-bit “Rapsberry Pi OS”, and the one that we are interested in is the second one.

Direct link for the latest version at the time of writing (January 2023) is here.

Download the image and write it to an SD card, and you are ready!

Install vulkan drivers

Even recent (relative to the time of writing) resources on installing Vulkan on Raspberry Pi (e.g., this one) suggest to install Mesa from scratch, either using PiKISS or by building it manually.

In both cases, the latest version of mesa branch 20.3 is built locally and installed over the system-provided version of mesa. However, looking at the commit history in that branch, it seems that at the time of writing, the last commits were for making the official 20.3.5 release.

And debian bullseye already comes with packaged mesa 20.3.5.

So the only thing required for enabling Vulkan on the system installed from the 2022-01-28-raspios-bullseye-arm64 image is to install the mesa-vulkan-drivers package:

sudo apt-get update
sudo apt-get install mesa-vulkan-drivers vulkan-tools

You can also install Vulkan validation layers, which come handy when debugging Vulkan programs:

sudo apt-get install vulkan-validationlayers

Test the vulkan setup

At this point, the vulkan should be ready to use, with V3D 4.2 device being available in addition to the llvmpipe software renderer:

pi@avalon:~ $ vulkaninfo

WARNING: v3dv is neither a complete nor a conformant Vulkan implementation. Testing use only.
WARNING: lavapipe is not a conformant vulkan implementation, testing use only.
==========
VULKANINFO
==========

Vulkan Instance Version: 1.2.162


Instance Extensions: count = 18
===============================
	VK_EXT_acquire_xlib_display            : extension revision 1
	VK_EXT_debug_report                    : extension revision 9
	VK_EXT_debug_utils                     : extension revision 2
	VK_EXT_direct_mode_display             : extension revision 1
	VK_EXT_display_surface_counter         : extension revision 1
	VK_KHR_device_group_creation           : extension revision 1
	VK_KHR_display                         : extension revision 23
	VK_KHR_external_fence_capabilities     : extension revision 1
	VK_KHR_external_memory_capabilities    : extension revision 1
	VK_KHR_external_semaphore_capabilities : extension revision 1
	VK_KHR_get_display_properties2         : extension revision 1
	VK_KHR_get_physical_device_properties2 : extension revision 1
	VK_KHR_get_surface_capabilities2       : extension revision 1
	VK_KHR_surface                         : extension revision 25
	VK_KHR_surface_protected_capabilities  : extension revision 1
	VK_KHR_wayland_surface                 : extension revision 6
	VK_KHR_xcb_surface                     : extension revision 6
	VK_KHR_xlib_surface                    : extension revision 6

Layers: count = 3
=================
VK_LAYER_KHRONOS_validation (Khronos Validation Layer) Vulkan version 1.2.162, layer version 1:
	Layer Extensions: count = 3
		VK_EXT_debug_report        : extension revision 9
		VK_EXT_debug_utils         : extension revision 1
		VK_EXT_validation_features : extension revision 2
	Devices: count = 2
		GPU id = 0 (V3D 4.2)
		Layer-Device Extensions: count = 3
			VK_EXT_debug_marker     : extension revision 4
			VK_EXT_tooling_info     : extension revision 1
			VK_EXT_validation_cache : extension revision 1

		GPU id = 1 (llvmpipe (LLVM 11.0.1, 128 bits))
		Layer-Device Extensions: count = 3
			VK_EXT_debug_marker     : extension revision 4
			VK_EXT_tooling_info     : extension revision 1
			VK_EXT_validation_cache : extension revision 1

VK_LAYER_MESA_device_select (Linux device selection layer) Vulkan version 1.2.73, layer version 1:
	Layer Extensions: count = 0
	Devices: count = 2
		GPU id = 0 (V3D 4.2)
		Layer-Device Extensions: count = 0

		GPU id = 1 (llvmpipe (LLVM 11.0.1, 128 bits))
		Layer-Device Extensions: count = 0

VK_LAYER_MESA_overlay (Mesa Overlay layer) Vulkan version 1.1.73, layer version 1:
	Layer Extensions: count = 0
	Devices: count = 2
		GPU id = 0 (V3D 4.2)
		Layer-Device Extensions: count = 0

		GPU id = 1 (llvmpipe (LLVM 11.0.1, 128 bits))
		Layer-Device Extensions: count = 0

Presentable Surfaces:
=====================
GPU id : 0 (V3D 4.2):
	Surface types: count = 2
		VK_KHR_xcb_surface
		VK_KHR_xlib_surface
	Formats: count = 2
		SurfaceFormat[0]:
			format = FORMAT_B8G8R8A8_SRGB
			colorSpace = COLOR_SPACE_SRGB_NONLINEAR_KHR
		SurfaceFormat[1]:
			format = FORMAT_B8G8R8A8_UNORM
			colorSpace = COLOR_SPACE_SRGB_NONLINEAR_KHR
	Present Modes: count = 4
		PRESENT_MODE_IMMEDIATE_KHR
		PRESENT_MODE_MAILBOX_KHR
		PRESENT_MODE_FIFO_KHR
		PRESENT_MODE_FIFO_RELAXED_KHR
	VkSurfaceCapabilitiesKHR:
	-------------------------
		minImageCount       = 3
		maxImageCount       = 0
		currentExtent:
			width  = 256
			height = 256
		minImageExtent:
			width  = 256
			height = 256
		maxImageExtent:
			width  = 256
			height = 256
		maxImageArrayLayers = 1
		supportedTransforms: count = 1
			SURFACE_TRANSFORM_IDENTITY_BIT_KHR
		currentTransform    = SURFACE_TRANSFORM_IDENTITY_BIT_KHR
		supportedCompositeAlpha: count = 2
			COMPOSITE_ALPHA_OPAQUE_BIT_KHR
			COMPOSITE_ALPHA_INHERIT_BIT_KHR
		supportedUsageFlags: count = 4
			IMAGE_USAGE_TRANSFER_SRC_BIT
			IMAGE_USAGE_TRANSFER_DST_BIT
			IMAGE_USAGE_STORAGE_BIT
			IMAGE_USAGE_COLOR_ATTACHMENT_BIT
	VkSurfaceCapabilities2EXT:
	--------------------------
		supportedSurfaceCounters: count = 0
			None
	VkSurfaceProtectedCapabilitiesKHR:
	----------------------------------
		supportsProtected = false


GPU id : 1 (llvmpipe (LLVM 11.0.1, 128 bits)):
	Surface types: count = 2
		VK_KHR_xcb_surface
		VK_KHR_xlib_surface
	Formats: count = 2
		SurfaceFormat[0]:
			format = FORMAT_B8G8R8A8_SRGB
			colorSpace = COLOR_SPACE_SRGB_NONLINEAR_KHR
		SurfaceFormat[1]:
			format = FORMAT_B8G8R8A8_UNORM
			colorSpace = COLOR_SPACE_SRGB_NONLINEAR_KHR
	Present Modes: count = 4
		PRESENT_MODE_IMMEDIATE_KHR
		PRESENT_MODE_MAILBOX_KHR
		PRESENT_MODE_FIFO_KHR
		PRESENT_MODE_FIFO_RELAXED_KHR
	VkSurfaceCapabilitiesKHR:
	-------------------------
		minImageCount       = 3
		maxImageCount       = 0
		currentExtent:
			width  = 256
			height = 256
		minImageExtent:
			width  = 256
			height = 256
		maxImageExtent:
			width  = 256
			height = 256
		maxImageArrayLayers = 1
		supportedTransforms: count = 1
			SURFACE_TRANSFORM_IDENTITY_BIT_KHR
		currentTransform    = SURFACE_TRANSFORM_IDENTITY_BIT_KHR
		supportedCompositeAlpha: count = 2
			COMPOSITE_ALPHA_OPAQUE_BIT_KHR
			COMPOSITE_ALPHA_INHERIT_BIT_KHR
		supportedUsageFlags: count = 5
			IMAGE_USAGE_TRANSFER_SRC_BIT
			IMAGE_USAGE_TRANSFER_DST_BIT
			IMAGE_USAGE_SAMPLED_BIT
			IMAGE_USAGE_STORAGE_BIT
			IMAGE_USAGE_COLOR_ATTACHMENT_BIT
	VkSurfaceCapabilities2EXT:
	--------------------------
		supportedSurfaceCounters: count = 0
			None
	VkSurfaceProtectedCapabilitiesKHR:
	----------------------------------
		supportsProtected = false



Device Groups:
==============
Group 0:
	Properties:
		physicalDevices: count = 1
			llvmpipe (LLVM 11.0.1, 128 bits) (ID: 0)
		subsetAllocation = 0

	Present Capabilities = Group does not support VK_KHR_device_group, skipping printing present capabilities

Group 1:
	Properties:
		physicalDevices: count = 1
			V3D 4.2 (ID: 0)
		subsetAllocation = 0

	Present Capabilities = Group does not support VK_KHR_device_group, skipping printing present capabilities


Device Properties and Extensions:
=================================
GPU0:
VkPhysicalDeviceProperties:
---------------------------
	apiVersion     = 4194459 (1.0.155)
	driverVersion  = 83898373 (0x5003005)
	vendorID       = 0x14e4
	deviceID       = 0x002a
	deviceType     = PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU
	deviceName     = V3D 4.2

VkPhysicalDeviceLimits:
-----------------------
	maxImageDimension1D                             = 4096
	maxImageDimension2D                             = 4096
	maxImageDimension3D                             = 4096
	maxImageDimensionCube                           = 4096
	maxImageArrayLayers                             = 2048
	maxTexelBufferElements                          = 268435456
	maxUniformBufferRange                           = 134217728
	maxStorageBufferRange                           = 134217728
	maxPushConstantsSize                            = 128
	maxMemoryAllocationCount                        = 431003
	maxSamplerAllocationCount                       = 65536
	bufferImageGranularity                          = 0x00000100
	sparseAddressSpaceSize                          = 0x00000000
	maxBoundDescriptorSets                          = 16
	maxPerStageDescriptorSamplers                   = 16
	maxPerStageDescriptorUniformBuffers             = 12
	maxPerStageDescriptorStorageBuffers             = 12
	maxPerStageDescriptorSampledImages              = 16
	maxPerStageDescriptorStorageImages              = 4
	maxPerStageDescriptorInputAttachments           = 4
	maxPerStageResources                            = 128
	maxDescriptorSetSamplers                        = 96
	maxDescriptorSetUniformBuffers                  = 72
	maxDescriptorSetUniformBuffersDynamic           = 8
	maxDescriptorSetStorageBuffers                  = 72
	maxDescriptorSetStorageBuffersDynamic           = 36
	maxDescriptorSetSampledImages                   = 96
	maxDescriptorSetStorageImages                   = 24
	maxDescriptorSetInputAttachments                = 4
	maxVertexInputAttributes                        = 16
	maxVertexInputBindings                          = 16
	maxVertexInputAttributeOffset                   = 4294967295
	maxVertexInputBindingStride                     = 4294967295
	maxVertexOutputComponents                       = 64
	maxTessellationGenerationLevel                  = 0
	maxTessellationPatchSize                        = 0
	maxTessellationControlPerVertexInputComponents  = 0
	maxTessellationControlPerVertexOutputComponents = 0
	maxTessellationControlPerPatchOutputComponents  = 0
	maxTessellationControlTotalOutputComponents     = 0
	maxTessellationEvaluationInputComponents        = 0
	maxTessellationEvaluationOutputComponents       = 0
	maxGeometryShaderInvocations                    = 0
	maxGeometryInputComponents                      = 0
	maxGeometryOutputComponents                     = 0
	maxGeometryOutputVertices                       = 0
	maxGeometryTotalOutputComponents                = 0
	maxFragmentInputComponents                      = 64
	maxFragmentOutputAttachments                    = 4
	maxFragmentDualSrcAttachments                   = 0
	maxFragmentCombinedOutputResources              = 20
	maxComputeSharedMemorySize                      = 16384
	maxComputeWorkGroupCount: count = 3
		65535
		65535
		65535
	maxComputeWorkGroupInvocations                  = 256
	maxComputeWorkGroupSize: count = 3
		256
		256
		256
	subPixelPrecisionBits                           = 6
	subTexelPrecisionBits                           = 8
	mipmapPrecisionBits                             = 8
	maxDrawIndexedIndexValue                        = 16777215
	maxDrawIndirectCount                            = 2147483647
	maxSamplerLodBias                               = 14
	maxSamplerAnisotropy                            = 16
	maxViewports                                    = 1
	maxViewportDimensions: count = 2
		4096
		4096
	viewportBoundsRange: count = 2
		-8192
		8191
	viewportSubPixelBits                            = 0
	minMemoryMapAlignment                           = 4096
	minTexelBufferOffsetAlignment                   = 0x00000100
	minUniformBufferOffsetAlignment                 = 0x00000020
	minStorageBufferOffsetAlignment                 = 0x00000020
	minTexelOffset                                  = -8
	maxTexelOffset                                  = 7
	minTexelGatherOffset                            = -8
	maxTexelGatherOffset                            = 7
	minInterpolationOffset                          = -0.5
	maxInterpolationOffset                          = 0.5
	subPixelInterpolationOffsetBits                 = 6
	maxFramebufferWidth                             = 4096
	maxFramebufferHeight                            = 4096
	maxFramebufferLayers                            = 256
	framebufferColorSampleCounts: count = 2
		SAMPLE_COUNT_1_BIT
		SAMPLE_COUNT_4_BIT
	framebufferDepthSampleCounts: count = 2
		SAMPLE_COUNT_1_BIT
		SAMPLE_COUNT_4_BIT
	framebufferStencilSampleCounts: count = 2
		SAMPLE_COUNT_1_BIT
		SAMPLE_COUNT_4_BIT
	framebufferNoAttachmentsSampleCounts: count = 2
		SAMPLE_COUNT_1_BIT
		SAMPLE_COUNT_4_BIT
	maxColorAttachments                             = 4
	sampledImageColorSampleCounts: count = 2
		SAMPLE_COUNT_1_BIT
		SAMPLE_COUNT_4_BIT
	sampledImageIntegerSampleCounts: count = 2
		SAMPLE_COUNT_1_BIT
		SAMPLE_COUNT_4_BIT
	sampledImageDepthSampleCounts: count = 2
		SAMPLE_COUNT_1_BIT
		SAMPLE_COUNT_4_BIT
	sampledImageStencilSampleCounts: count = 2
		SAMPLE_COUNT_1_BIT
		SAMPLE_COUNT_4_BIT
	storageImageSampleCounts: count = 1
		SAMPLE_COUNT_1_BIT
	maxSampleMaskWords                              = 1
	timestampComputeAndGraphics                     = true
	timestampPeriod                                 = 1
	maxClipDistances                                = 8
	maxCullDistances                                = 0
	maxCombinedClipAndCullDistances                 = 8
	discreteQueuePriorities                         = 2
	pointSizeRange: count = 2
		0
		512
	lineWidthRange: count = 2
		1
		32
	pointSizeGranularity                            = 0
	lineWidthGranularity                            = 0
	strictLines                                     = true
	standardSampleLocations                         = false
	optimalBufferCopyOffsetAlignment                = 0x00000020
	optimalBufferCopyRowPitchAlignment              = 0x00000020
	nonCoherentAtomSize                             = 0x00000100

VkPhysicalDeviceSparseProperties:
---------------------------------
	residencyStandard2DBlockShape            = false
	residencyStandard2DMultisampleBlockShape = false
	residencyStandard3DBlockShape            = false
	residencyAlignedMipSize                  = false
	residencyNonResidentStrict               = false

VkPhysicalDeviceIDPropertiesKHR:
--------------------------------
	deviceUUID      = e8c1c12d-2325-2af0-b0e8-66d7e87e7189
	driverUUID      = ed5483eb-9eb8-fe23-3b21-a3d24026d778
	deviceNodeMask  = 0
	deviceLUIDValid = false


Device Extensions: count = 5
----------------------------
	VK_EXT_external_memory_dma_buf : extension revision 1
	VK_KHR_external_memory         : extension revision 1
	VK_KHR_external_memory_fd      : extension revision 1
	VK_KHR_maintenance1            : extension revision 2
	VK_KHR_swapchain               : extension revision 68

VkQueueFamilyProperties:
========================
	queueProperties[0]:
	-------------------
		minImageTransferGranularity = (1,1,1)
		queueCount                  = 1
		queueFlags                  = QUEUE_GRAPHICS | QUEUE_COMPUTE | QUEUE_TRANSFER
		timestampValidBits          = 64
		present support             = true

VkPhysicalDeviceMemoryProperties:
=================================
memoryHeaps: count = 1
	memoryHeaps[0]:
		size   = 1765388288 (0x6939b000) (1.64 GiB)
		budget = 103079215104 (0x1800000000) (96.00 GiB)
		usage  = 103079215104 (0x1800000000) (96.00 GiB)
		flags: count = 1
			MEMORY_HEAP_DEVICE_LOCAL_BIT
memoryTypes: count = 1
	memoryTypes[0]:
		heapIndex     = 0
		propertyFlags = 0x0007: count = 3
			MEMORY_PROPERTY_DEVICE_LOCAL_BIT
			MEMORY_PROPERTY_HOST_VISIBLE_BIT
			MEMORY_PROPERTY_HOST_COHERENT_BIT
		usable for:
			IMAGE_TILING_OPTIMAL:
				color images
				FORMAT_D16_UNORM
				FORMAT_X8_D24_UNORM_PACK32
				FORMAT_D32_SFLOAT
				FORMAT_D24_UNORM_S8_UINT
				(non-sparse)
			IMAGE_TILING_LINEAR:
				color images
				(non-sparse)

VkPhysicalDeviceFeatures:
=========================
	robustBufferAccess                      = true
	fullDrawIndexUint32                     = false
	imageCubeArray                          = true
	independentBlend                        = true
	geometryShader                          = false
	tessellationShader                      = false
	sampleRateShading                       = true
	dualSrcBlend                            = false
	logicOp                                 = true
	multiDrawIndirect                       = false
	drawIndirectFirstInstance               = true
	depthClamp                              = false
	depthBiasClamp                          = false
	fillModeNonSolid                        = true
	depthBounds                             = false
	wideLines                               = true
	largePoints                             = true
	alphaToOne                              = true
	multiViewport                           = false
	samplerAnisotropy                       = true
	textureCompressionETC2                  = true
	textureCompressionASTC_LDR              = false
	textureCompressionBC                    = false
	occlusionQueryPrecise                   = true
	pipelineStatisticsQuery                 = false
	vertexPipelineStoresAndAtomics          = true
	fragmentStoresAndAtomics                = true
	shaderTessellationAndGeometryPointSize  = false
	shaderImageGatherExtended               = false
	shaderStorageImageExtendedFormats       = true
	shaderStorageImageMultisample           = false
	shaderStorageImageReadWithoutFormat     = false
	shaderStorageImageWriteWithoutFormat    = false
	shaderUniformBufferArrayDynamicIndexing = false
	shaderSampledImageArrayDynamicIndexing  = false
	shaderStorageBufferArrayDynamicIndexing = false
	shaderStorageImageArrayDynamicIndexing  = false
	shaderClipDistance                      = true
	shaderCullDistance                      = false
	shaderFloat64                           = false
	shaderInt64                             = false
	shaderInt16                             = false
	shaderResourceResidency                 = false
	shaderResourceMinLod                    = false
	sparseBinding                           = false
	sparseResidencyBuffer                   = false
	sparseResidencyImage2D                  = false
	sparseResidencyImage3D                  = false
	sparseResidency2Samples                 = false
	sparseResidency4Samples                 = false
	sparseResidency8Samples                 = false
	sparseResidency16Samples                = false
	sparseResidencyAliased                  = false
	variableMultisampleRate                 = false
	inheritedQueries                        = true


GPU1:
-----
VkPhysicalDeviceProperties:
---------------------------
	apiVersion     = 4194306 (1.0.2)
	driverVersion  = 1 (0x0001)
	vendorID       = 0x10005
	deviceID       = 0x0000
	deviceType     = PHYSICAL_DEVICE_TYPE_CPU
	deviceName     = llvmpipe (LLVM 11.0.1, 128 bits)

VkPhysicalDeviceLimits:
-----------------------
	maxImageDimension1D                             = 16384
	maxImageDimension2D                             = 16384
	maxImageDimension3D                             = 4096
	maxImageDimensionCube                           = 32768
	maxImageArrayLayers                             = 2048
	maxTexelBufferElements                          = 134217728
	maxUniformBufferRange                           = 65536
	maxStorageBufferRange                           = 134217728
	maxPushConstantsSize                            = 128
	maxMemoryAllocationCount                        = 4096
	maxSamplerAllocationCount                       = 32768
	bufferImageGranularity                          = 0x00000040
	sparseAddressSpaceSize                          = 0x00000000
	maxBoundDescriptorSets                          = 8
	maxPerStageDescriptorSamplers                   = 32
	maxPerStageDescriptorUniformBuffers             = 16
	maxPerStageDescriptorStorageBuffers             = 16
	maxPerStageDescriptorSampledImages              = 128
	maxPerStageDescriptorStorageImages              = 128
	maxPerStageDescriptorInputAttachments           = 8
	maxPerStageResources                            = 128
	maxDescriptorSetSamplers                        = 32768
	maxDescriptorSetUniformBuffers                  = 256
	maxDescriptorSetUniformBuffersDynamic           = 256
	maxDescriptorSetStorageBuffers                  = 256
	maxDescriptorSetStorageBuffersDynamic           = 256
	maxDescriptorSetSampledImages                   = 256
	maxDescriptorSetStorageImages                   = 256
	maxDescriptorSetInputAttachments                = 256
	maxVertexInputAttributes                        = 32
	maxVertexInputBindings                          = 32
	maxVertexInputAttributeOffset                   = 2047
	maxVertexInputBindingStride                     = 2048
	maxVertexOutputComponents                       = 128
	maxTessellationGenerationLevel                  = 64
	maxTessellationPatchSize                        = 32
	maxTessellationControlPerVertexInputComponents  = 128
	maxTessellationControlPerVertexOutputComponents = 128
	maxTessellationControlPerPatchOutputComponents  = 128
	maxTessellationControlTotalOutputComponents     = 4096
	maxTessellationEvaluationInputComponents        = 128
	maxTessellationEvaluationOutputComponents       = 128
	maxGeometryShaderInvocations                    = 32
	maxGeometryInputComponents                      = 64
	maxGeometryOutputComponents                     = 128
	maxGeometryOutputVertices                       = 1024
	maxGeometryTotalOutputComponents                = 1024
	maxFragmentInputComponents                      = 128
	maxFragmentOutputAttachments                    = 8
	maxFragmentDualSrcAttachments                   = 2
	maxFragmentCombinedOutputResources              = 8
	maxComputeSharedMemorySize                      = 32768
	maxComputeWorkGroupCount: count = 3
		65535
		65535
		65535
	maxComputeWorkGroupInvocations                  = 1024
	maxComputeWorkGroupSize: count = 3
		1024
		1024
		1024
	subPixelPrecisionBits                           = 8
	subTexelPrecisionBits                           = 8
	mipmapPrecisionBits                             = 8
	maxDrawIndexedIndexValue                        = 4294967295
	maxDrawIndirectCount                            = 4294967295
	maxSamplerLodBias                               = 16
	maxSamplerAnisotropy                            = 16
	maxViewports                                    = 16
	maxViewportDimensions: count = 2
		16384
		16384
	viewportBoundsRange: count = 2
		-32768
		32768
	viewportSubPixelBits                            = 0
	minMemoryMapAlignment                           = 4096
	minTexelBufferOffsetAlignment                   = 0x00000010
	minUniformBufferOffsetAlignment                 = 0x00000010
	minStorageBufferOffsetAlignment                 = 0x00000010
	minTexelOffset                                  = -32
	maxTexelOffset                                  = 31
	minTexelGatherOffset                            = -32
	maxTexelGatherOffset                            = 31
	minInterpolationOffset                          = -2
	maxInterpolationOffset                          = 2
	subPixelInterpolationOffsetBits                 = 8
	maxFramebufferWidth                             = 16384
	maxFramebufferHeight                            = 16384
	maxFramebufferLayers                            = 2048
	framebufferColorSampleCounts: count = 2
		SAMPLE_COUNT_1_BIT
		SAMPLE_COUNT_4_BIT
	framebufferDepthSampleCounts: count = 2
		SAMPLE_COUNT_1_BIT
		SAMPLE_COUNT_4_BIT
	framebufferStencilSampleCounts: count = 2
		SAMPLE_COUNT_1_BIT
		SAMPLE_COUNT_4_BIT
	framebufferNoAttachmentsSampleCounts: count = 2
		SAMPLE_COUNT_1_BIT
		SAMPLE_COUNT_4_BIT
	maxColorAttachments                             = 8
	sampledImageColorSampleCounts: count = 2
		SAMPLE_COUNT_1_BIT
		SAMPLE_COUNT_4_BIT
	sampledImageIntegerSampleCounts: count = 2
		SAMPLE_COUNT_1_BIT
		SAMPLE_COUNT_4_BIT
	sampledImageDepthSampleCounts: count = 2
		SAMPLE_COUNT_1_BIT
		SAMPLE_COUNT_4_BIT
	sampledImageStencilSampleCounts: count = 2
		SAMPLE_COUNT_1_BIT
		SAMPLE_COUNT_4_BIT
	storageImageSampleCounts: count = 2
		SAMPLE_COUNT_1_BIT
		SAMPLE_COUNT_4_BIT
	maxSampleMaskWords                              = 1
	timestampComputeAndGraphics                     = true
	timestampPeriod                                 = 1
	maxClipDistances                                = 8
	maxCullDistances                                = 8
	maxCombinedClipAndCullDistances                 = 8
	discreteQueuePriorities                         = 2
	pointSizeRange: count = 2
		0
		255
	lineWidthRange: count = 2
		1
		1
	pointSizeGranularity                            = 0.125
	lineWidthGranularity                            = 0
	strictLines                                     = false
	standardSampleLocations                         = true
	optimalBufferCopyOffsetAlignment                = 0x00000080
	optimalBufferCopyRowPitchAlignment              = 0x00000080
	nonCoherentAtomSize                             = 0x00000040

VkPhysicalDeviceSparseProperties:
---------------------------------
	residencyStandard2DBlockShape            = false
	residencyStandard2DMultisampleBlockShape = false
	residencyStandard3DBlockShape            = false
	residencyAlignedMipSize                  = false
	residencyNonResidentStrict               = false

VkPhysicalDeviceDriverPropertiesKHR:
------------------------------------
	driverID           = DRIVER_ID_MESA_LLVMPIPE
	driverName         = llvmpipe
	driverInfo         = Mesa 20.3.5 (LLVM 11.0.1)
	conformanceVersion = 1.0.0.0

VkPhysicalDeviceIDPropertiesKHR:
--------------------------------
	deviceUUID      = 00000000-0000-0000-0000-000000000000
	driverUUID      = 00000000-0000-0000-0000-000000000000
	deviceNodeMask  = 0
	deviceLUIDValid = false


Device Extensions: count = 14
-----------------------------
	VK_EXT_external_memory_dma_buf      : extension revision 1
	VK_EXT_private_data                 : extension revision 1
	VK_GOOGLE_decorate_string           : extension revision 1
	VK_GOOGLE_hlsl_functionality1       : extension revision 1
	VK_KHR_bind_memory2                 : extension revision 1
	VK_KHR_dedicated_allocation         : extension revision 1
	VK_KHR_driver_properties            : extension revision 1
	VK_KHR_get_memory_requirements2     : extension revision 1
	VK_KHR_incremental_present          : extension revision 1
	VK_KHR_maintenance1                 : extension revision 1
	VK_KHR_relaxed_block_layout         : extension revision 1
	VK_KHR_sampler_mirror_clamp_to_edge : extension revision 1
	VK_KHR_storage_buffer_storage_class : extension revision 1
	VK_KHR_swapchain                    : extension revision 68

VkQueueFamilyProperties:
========================
	queueProperties[0]:
	-------------------
		minImageTransferGranularity = (1,1,1)
		queueCount                  = 1
		queueFlags                  = QUEUE_GRAPHICS | QUEUE_COMPUTE | QUEUE_TRANSFER
		timestampValidBits          = 64
		present support             = true

VkPhysicalDeviceMemoryProperties:
=================================
memoryHeaps: count = 1
	memoryHeaps[0]:
		size   = 2147483648 (0x80000000) (2.00 GiB)
		budget = 0 (0x00000000) (0.00 B)
		usage  = 0 (0x00000000) (0.00 B)
		flags: count = 1
			MEMORY_HEAP_DEVICE_LOCAL_BIT
memoryTypes: count = 1
	memoryTypes[0]:
		heapIndex     = 0
		propertyFlags = 0x000f: count = 4
			MEMORY_PROPERTY_DEVICE_LOCAL_BIT
			MEMORY_PROPERTY_HOST_VISIBLE_BIT
			MEMORY_PROPERTY_HOST_COHERENT_BIT
			MEMORY_PROPERTY_HOST_CACHED_BIT
		usable for:
			IMAGE_TILING_OPTIMAL:
				color images
				FORMAT_D16_UNORM
				FORMAT_X8_D24_UNORM_PACK32
				FORMAT_D32_SFLOAT
				FORMAT_S8_UINT
				FORMAT_D24_UNORM_S8_UINT
				FORMAT_D32_SFLOAT_S8_UINT
				(non-sparse)
			IMAGE_TILING_LINEAR:
				color images
				(non-sparse)

VkPhysicalDeviceFeatures:
=========================
	robustBufferAccess                      = true
	fullDrawIndexUint32                     = true
	imageCubeArray                          = true
	independentBlend                        = true
	geometryShader                          = true
	tessellationShader                      = true
	sampleRateShading                       = true
	dualSrcBlend                            = true
	logicOp                                 = true
	multiDrawIndirect                       = true
	drawIndirectFirstInstance               = true
	depthClamp                              = true
	depthBiasClamp                          = true
	fillModeNonSolid                        = true
	depthBounds                             = false
	wideLines                               = false
	largePoints                             = true
	alphaToOne                              = true
	multiViewport                           = true
	samplerAnisotropy                       = false
	textureCompressionETC2                  = false
	textureCompressionASTC_LDR              = false
	textureCompressionBC                    = true
	occlusionQueryPrecise                   = true
	pipelineStatisticsQuery                 = true
	vertexPipelineStoresAndAtomics          = true
	fragmentStoresAndAtomics                = true
	shaderTessellationAndGeometryPointSize  = true
	shaderImageGatherExtended               = true
	shaderStorageImageExtendedFormats       = false
	shaderStorageImageMultisample           = true
	shaderStorageImageReadWithoutFormat     = false
	shaderStorageImageWriteWithoutFormat    = true
	shaderUniformBufferArrayDynamicIndexing = false
	shaderSampledImageArrayDynamicIndexing  = false
	shaderStorageBufferArrayDynamicIndexing = false
	shaderStorageImageArrayDynamicIndexing  = false
	shaderClipDistance                      = true
	shaderCullDistance                      = true
	shaderFloat64                           = true
	shaderInt64                             = true
	shaderInt16                             = true
	shaderResourceResidency                 = false
	shaderResourceMinLod                    = false
	sparseBinding                           = false
	sparseResidencyBuffer                   = false
	sparseResidencyImage2D                  = false
	sparseResidencyImage3D                  = false
	sparseResidency2Samples                 = false
	sparseResidency4Samples                 = false
	sparseResidency8Samples                 = false
	sparseResidency16Samples                = false
	sparseResidencyAliased                  = false
	variableMultisampleRate                 = false
	inheritedQueries                        = false

VkPhysicalDevicePrivateDataFeaturesEXT:
---------------------------------------
	privateData = true

And running the vkcube demo should also work:

pi@avalon:~ $ vkcube

vkcube screenshot

Install and set up dotnet

Installing dotnet on Raspberry Pi is a rather straightforward affair. The instructions here are loosely based on instructions from here.

Go to Microsoft’s .NET 6.0 download page, and download the latest SDK 6.0 release for arm64.

Then, create a directory called dotnet in your home directory, and unpack the SDK(s) into it, i.e.,

mkdir -p $HOME/dotnet 

tar xvf dotnet-sdk-6.0.101-linux-arm64.tar.gz -C $HOME/dotnet/

Then open the .bashrc file in your home directory using your favorite text editor (e.g., vi ~/.bashrc) and add the following at the end:

# dotnet
export DOTNET_ROOT=$HOME/dotnet
export PATH=$PATH:$HOME/dotnet
export DOTNET_CLI_TELEMETRY_OPTOUT=1

This sets up the dotnet path (and opts out from the CLI telemetry). The path becomes active on the next log-in, or immediately if you re-source the file (i.e., run . ~/.bashrc from the shell).

After ensuring that dotnet path is active (see above), you should see the installed SDK(s):

pi@avalon:~ $ dotnet --info
.NET SDK (reflecting any global.json):
 Version:   6.0.101
 Commit:    ef49f6213a

Runtime Environment:
 OS Name:     debian
 OS Version:  11
 OS Platform: Linux
 RID:         debian.11-arm64
 Base Path:   /home/pi/dotnet/sdk/6.0.101/

Host (useful for support):
  Version: 6.0.1
  Commit:  3a25a7f1cc

.NET SDKs installed:
  6.0.101 [/home/pi/dotnet/sdk]

.NET runtimes installed:
  Microsoft.AspNetCore.App 6.0.1 [/home/pi/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 6.0.1 [/home/pi/dotnet/shared/Microsoft.NETCore.App]

To install additional .NET runtimes or SDKs:
  https://aka.ms/dotnet-download

and

pi@avalon:~ $ dotnet --list-sdks
5.0.404 [/home/pi/dotnet/sdk]
6.0.101 [/home/pi/dotnet/sdk]

You can now create and run a hello-world application:

pi@avalon:~ $ dotnet new console -o HelloWorld
The template "Console App" was created successfully.

Processing post-creation actions...
Running 'dotnet restore' on /home/pi/HelloWorld/HelloWorld.csproj...
  Determining projects to restore...
  Restored /home/pi/HelloWorld/HelloWorld.csproj (in 406 ms).
Restore succeeded.

pi@avalon:~ $ cd HelloWorld/
pi@avalon:~/HelloWorld $ dotnet run
Hello, World!

Run the Ab4d.SharpEngine demo

The Ab4d.SharpEngine demo uses Silk windowing library, which requires SDL2. In particular, it seems to look for libSDL2.so file that is provided by the libsdl2-dev package (which will pull in all other dependencies as well):

sudo apt-get install libsdl2-dev

You are now all set up to run the Ab4d.SharpEngine demo. Simply clone the code from GitHub Ab4d.SharpEngine.Samples:

pi@avalon:~ $ git clone https://github.com/ab4d/Ab4d.SharpEngine.Samples.git
Cloning into 'Ab4d.SharpEngine.Samples'...
remote: Enumerating objects: 317, done.
remote: Counting objects: 100% (317/317), done.
remote: Compressing objects: 100% (213/213), done.
remote: Total 317 (delta 103), reused 299 (delta 91), pack-reused 0
Receiving objects: 100% (317/317), 19.00 MiB | 3.22 MiB/s, done.
Resolving deltas: 100% (103/103), done.

The Ab4d.SharpEngine.Samples repository has multiple solutions, but two can also run on Raspberry Pi 4:

1) Ab4d.SharpEngine.Samples.CrossPlatform.sln
This solution uses SDL for creating windows and for processing user input.

NOTE:
When using Ab4d.SharpEngine v0.9.0-beta1, open the SharpEngineCrossPlatformSamplesRunner.cs and add the following into line 298:
engineCreateOptions.RequiredInstanceExtensionNames.Add("VK_KHR_get_physical_device_properties2");

To start this sample, first move to the CrossPlatform project directory …

pi@avalon:~ $ cd Ab4d.SharpEngine.Samples/Ab4d.SharpEngine.Samples.CrossPlatform/
pi@avalon:~/Ab4d.SharpEngine.Samples/Ab4d.SharpEngine.Samples.CrossPlatform $

… and run the program (and wait a while for it to compile):

pi@avalon:~/Ab4d.SharpEngine.Samples/Ab4d.SharpEngine.Samples.CrossPlatform $ dotnet run

Vulkan based Ab4d.SharpEngine screenshot on Raspberry Pi 4 - Cross-platform app

 

2) Ab4d.SharpEngine.Samples.AvaloniaUI.sln
This solution uses Avalonia UI as cross-platform UI framework.

NOTE:
When using Ab4d.SharpEngine v0.9.0-beta1, open the MainWindow.axaml.cs and add the following into line 66:
SharpEngineSceneView.CreateOptions.RequiredInstanceExtensionNames.Add("VK_KHR_get_physical_device_properties2");

To start this sample, first move to its directory and run the sample by executing dotnet run. The following is a screenshot of the sample application:

Vulkan based Ab4d.SharpEngine screenshot on Raspberry Pi 4 - Avalonia UI app

 

Part 2: using an attached LCD panel

In the first part of this post, we have seen how to set up Vulkan and dotnet on the latest, 64-bit version of the Raspberry Pi OS running on Raspberry Pi 4. In this part, we will explore the possibility of replacing an external PC monitor with an attached LCD panel, which makes for an attractive option for running 3D applications on stand-alone embedded devices.

The LCD screen

The particular LCD screen used in this post is Miuzei Raspberry Pi 4 Touchscreen, a 4-inch IPS touchscreen display with resolution of 800x480.

The first run

After assembling the display following the enclosed instructions, and turning on the Pi, we are greeted by a blue screen that flickers (changes between blue and black) several times as the Pi undergoes its boot process, until the user’s X session is (auto)started. At that point, the desktop is displayed, albeit in portrait mode instead of the landscape one.

This is actually expected and pointed out in the enclosed instructions, telling us that we need to install the touchscreen’s driver:

miuzei instructions

Driver installation

So, we install the driver - but not before creating a backup of the SD card!

The instructions boil down to cloning a public repository, and running a script from it:

sudo rm -rf LCD-show
git clone https://github.com/goodtft/LCD-show
chmod -R 755 LCD-show
cd LCD-show/
sudo ./MPI4008-show

After the script finishes its setup, it restarts the Pi. While the Pi is shutting down, we go black to blue-and-black flickering, but once it restarts, we get the display right off the bat.

The flicker during boot is gone; we can see the RPi splash screen, the boot progress, and finally, we get the X session in landscape orientation. Plus, the touch screen works.

Profit?

There’s only one problem - we’ve lost the Vulkan functionality. Trying to run vkcube again, we are greeted by:

pi@avalon:~ $ vkcube
vulkan: No DRI3 support detected - required for presentation
Note: you can probably enable DRI3 in your Xorg config
Could not find both graphics and present queues

So what happened here?

Driver installation, analyzed

To understand the problem we are facing, we need to take a closer look at the installation script that we just ran - its source can be seen here.

A quick summary:

  1. create a backup of current system files (line 3)
  2. choose a boot config template, based on the OS (normal Raspbian vs Noobs) (line 6 - 11)
  3. append several parameters to the selected boot config template (line 12 - 24)
  4. replace /boot/config.txt with the generated one (line 25)
  5. create/replace /usr/share/X11/xorg.conf.d/99-fbturbo.conf with custom copy (line 35)
  6. copy touch-screen calibration settings to /etc/X11/xorg.conf.d/99-calibration.conf. Based on the name, this calibration corresponds to 270° screen rotation. (line 39)
  7. store the settings to local file, in case we re-run any of the scripts (line 40-41)
  8. on newer Raspbian versions, ensure xserver-xorg-input-evdev is installed (and if the hardcoded repository mirror is unreachable, fall back to local copy of the package) (lines 50-63)
  9. create a copy of (system’s) /usr/share/X11/xorg.conf.d/10-evdev.conf file to /usr/share/X11/xorg.conf.d/45-evdev.conf
  10. if we passed the screen orientation as parameter to script (we did not), apply settings for that rotation (line 74-78)
  11. reboot (line 81)

The crucial part for our Vulkan woes is not immediately obvious; but the boot config file template used by the script disables the VC3 V3D driver, which causes the system to fall back to the basic framebuffer driver:

[pi4]
# Enable DRM VC4 V3D driver on top of the dispmanx display stack
#dtoverlay=vc4-fkms-v3d
max_framebuffers=2

We can try re-enabling the driver by modifying the corresponding line in /boot/config.txt back to its original value,

[pi4]
# Enable DRM VC4 V3D driver
dtoverlay=vc4-kms-v3d
max_framebuffers=2

(Sidenote: the original RPi boot.config uses vc4-kms-v3d instead of template’s older vc4-fkms-v3d, and had those settings placed outside of the [pi4] section. But this makes no difference, as we are on Pi 4.)

Re-enabling VC4 V3D driver and rebooting brings back blue-and-black flicker during boot, and portrait orientation in X session. So flicker-free display operation is predicated on the use of the framebuffer display driver (as apparently, V3D driver replaces that part of the firmware stack), and that precludes us from having a functional Vulkan driver.

But can we salvage the X session itself, so that we have landscape orientation, working touchscreen, and Vulkan acceleration?

Driver installation, revisited

While we could try to undo the relevant steps of the driver installation script, it is generally cleaner and more instructive to revert to the SD card backup, and start from a clean system.

First, clone the driver repository again, but do not run the installation script:

git clone https://github.com/goodtft/LCD-show

Now, we will modify the /boot/config.txt files with the extra settings from the installation script - but without disabling V3D driver. Open /boot/config.txt (as root) in your favorite text editor, and append the following lines at the end of the [all] section, which should be at the end of the file, and empty:

[all]
# Options for Miuzei 4.0 inch display
# Taken from MPI4008-show script of goodtft/LCD-show
hdmi_force_hotplug=1
hdmi_force_edid_audio=1
dtparam=i2c_arm=on
dtparam=spi=on
enable_uart=1
display_rotate=3
max_usb_current=1
config_hdmi_boost=7
hdmi_group=2
hdmi_mode=1
hdmi_mode=87
hdmi_drive=2
hdmi_cvt=480 800 60 6 0 0 0
dtoverlay=ads7846,cs=1,penirq=25,penirq_pull=2,speed=50000,keep_vref_on=0,swapxy=0,pmax=255,xohms=150,xmin=200,xmax=3900,ymin=200,ymax=3900

Ensure that the xserver-xorg-input-evdev package is installed:

sudo apt-get update
sudo apt-get install xserver-xorg-input-evdev

For touch screen to work, it seems we indeed need to make a copy of /usr/share/X11/xorg.conf.d/10-evdev.conf file to /usr/share/X11/xorg.conf.d/45-evdev.conf. This seems to imply that some settings are overwritten after the first file (10-evdev.conf) is loaded, and we ensure they are re-instated by having the file re-loaded again (as 45-evdev.conf).

Instead of copying the file, we can create a symbolic link:

sudo ln -s /usr/share/X11/xorg.conf.d/10-evdev.conf /usr/share/X11/xorg.conf.d/45-evdev.conf

Copy the touch screen calibration file (in our case for 270° rotation) to /etc/X11/xorg.conf.d/99-calibration.conf.

If we restart the Pi now, we still end up with portrait-oriented display. To remedy that, we can rotate the display using xrandr:

xrandr -o left

To make the rotation permanent, run arandr utility as root:

sudo arandr

Set up the desired orientation (should already be set following the previous xrandr command), and export the configuration by selecting Configure->Apply. This utility writes a script called /usr/share/dispsetup.sh that is used at X session startup to automatically rotate the display into the target position.

Profit!

After the reboot, you should end up with landscape-oriented display that is matched by the working touch screen. The boot sequence is still flickering, but we now have a Vulkan acceleration available in the X session:

Photo of cross-plaform Vulkan based Ab4d.SharpEngine on Raspberry Pi 4 with a touch screen

Photo of Avalonia UI Vulkan based Ab4d.SharpEngine on Raspberry Pi 4 with a touch screen