1 | Dispatch
|
---|
2 | =============
|
---|
3 |
|
---|
4 | This chapter attempts to document the Vulkan dispatch infrastructure in the
|
---|
5 | Mesa Vulkan runtime. There are a lot of moving pieces here but the end
|
---|
6 | result has proven quite effective for implementing all the various Vulkan
|
---|
7 | API requirements.
|
---|
8 |
|
---|
9 |
|
---|
10 | Extension tables
|
---|
11 | ----------------
|
---|
12 |
|
---|
13 | The Vulkan runtime defines two extension table structures, one for instance
|
---|
14 | extensions and one for device extensions which contain a Boolean per
|
---|
15 | extension. The device table looks like this:
|
---|
16 |
|
---|
17 | .. code-block:: c
|
---|
18 |
|
---|
19 | #define VK_DEVICE_EXTENSION_COUNT 238
|
---|
20 |
|
---|
21 | struct vk_device_extension_table {
|
---|
22 | union {
|
---|
23 | bool extensions[VK_DEVICE_EXTENSION_COUNT];
|
---|
24 | struct {
|
---|
25 | bool KHR_8bit_storage;
|
---|
26 | bool KHR_16bit_storage;
|
---|
27 | bool KHR_acceleration_structure;
|
---|
28 | bool KHR_bind_memory2;
|
---|
29 | ...
|
---|
30 | };
|
---|
31 | };
|
---|
32 | };
|
---|
33 |
|
---|
34 | The instance extension table is similar except that it includes the
|
---|
35 | instance level extensions. Both tables are actually unions so that you can
|
---|
36 | access the table either by name or as an array. Accessing by name is
|
---|
37 | typically better for human-written code which needs to query for specific
|
---|
38 | enabled extensions or declare a table of which extensions a driver
|
---|
39 | supports. The array form is convenient for more automatic code which wants
|
---|
40 | to iterate over the table.
|
---|
41 |
|
---|
42 | These tables are are generated automatically using a bit of python code that
|
---|
43 | parses the vk.xml from the `Vulkan-Docs repo
|
---|
44 | <https://github.com/KhronosGroup/Vulkan-docs/>`__, enumerates the
|
---|
45 | extensions, sorts them by instance vs. device and generates the table.
|
---|
46 | Generating it from XML means that we never have to manually maintain any of
|
---|
47 | these data structures; they get automatically updated when someone imports
|
---|
48 | a new version of vk.xml. We also generates a matching pair of tables of
|
---|
49 | ``VkExtensionProperties``. This makes it easy to implement
|
---|
50 | ``vkEnumerate*ExtensionProperties()`` with a simple loop that walks a table
|
---|
51 | of supported extensions and copies the VkExtensionProperties for each
|
---|
52 | enabled entry. Similarly, we can have a loop in ``vkCreateInstance()`` or
|
---|
53 | ``vkCreateDevice()`` which takes the ``ppEnabledExtensionNames`` and fills
|
---|
54 | out the table with all enabled extensions.
|
---|
55 |
|
---|
56 |
|
---|
57 | Entrypoint and dispatch tables
|
---|
58 | ------------------------------
|
---|
59 |
|
---|
60 | Entrypoint tables contain a function pointer for every Vulkan entrypoint
|
---|
61 | within a particular scope. There are separate tables for instance,
|
---|
62 | physical device, and device-level functionality. The device entrypoint
|
---|
63 | table looks like this:
|
---|
64 |
|
---|
65 | .. code-block:: c
|
---|
66 |
|
---|
67 | struct vk_device_entrypoint_table {
|
---|
68 | PFN_vkGetDeviceProcAddr GetDeviceProcAddr;
|
---|
69 | PFN_vkDestroyDevice DestroyDevice;
|
---|
70 | PFN_vkGetDeviceQueue GetDeviceQueue;
|
---|
71 | PFN_vkQueueSubmit QueueSubmit;
|
---|
72 | ...
|
---|
73 | #ifdef VK_USE_PLATFORM_WIN32_KHR
|
---|
74 | PFN_vkGetSemaphoreWin32HandleKHR GetSemaphoreWin32HandleKHR;
|
---|
75 | #else
|
---|
76 | PFN_vkVoidFunction GetSemaphoreWin32HandleKHR;
|
---|
77 | # endif
|
---|
78 | ...
|
---|
79 | };
|
---|
80 |
|
---|
81 | Every entry that requires some sort of platform define is wrapped in an
|
---|
82 | ``#ifdef`` and declared as the actual function pointer type if the platform
|
---|
83 | define is set and declared as a void function otherwise. This ensures that
|
---|
84 | the layout of the structure doesn't change based on preprocessor symbols
|
---|
85 | but anyone who has the platform defines set gets the real prototype and
|
---|
86 | anyone who doesn't can use the table without needing to pull in all the
|
---|
87 | platform headers.
|
---|
88 |
|
---|
89 | Dispatch tables are similar to entrypoint tables except that they're
|
---|
90 | deduplicated so that aliased entrypoints have only one entry in the table.
|
---|
91 | The device dispatch table looks like this:
|
---|
92 |
|
---|
93 | .. code-block:: c
|
---|
94 |
|
---|
95 | struct vk_device_dispatch_table {
|
---|
96 | PFN_vkGetDeviceProcAddr GetDeviceProcAddr;
|
---|
97 | PFN_vkDestroyDevice DestroyDevice;
|
---|
98 | PFN_vkGetDeviceQueue GetDeviceQueue;
|
---|
99 | PFN_vkQueueSubmit QueueSubmit;
|
---|
100 | ...
|
---|
101 | union {
|
---|
102 | PFN_vkResetQueryPool ResetQueryPool;
|
---|
103 | PFN_vkResetQueryPoolEXT ResetQueryPoolEXT;
|
---|
104 | };
|
---|
105 | ...
|
---|
106 | };
|
---|
107 |
|
---|
108 | In order to allow code to use any of the aliases for a given entrypoint,
|
---|
109 | such entrypoints are wrapped in a union. This is important because we need
|
---|
110 | to be able to add new aliases potentially at any Vulkan release and we want
|
---|
111 | to do so without having to update all the driver code which uses one of the
|
---|
112 | newly aliased entrypoints. We could require that everyone use the first
|
---|
113 | name an entrypoint ever has but that gets weird if, for instance, it's
|
---|
114 | introduced in an EXT extension and some driver only ever implements the KHR
|
---|
115 | or core version of the feature. It's easier for everyone if we make all
|
---|
116 | the entrypoint names work.
|
---|
117 |
|
---|
118 | An entrypoint table can be converted to a dispatch table by compacting it
|
---|
119 | with one of the ``vk_*_dispatch_table_from_entrypoints()`` family of
|
---|
120 | functions:
|
---|
121 |
|
---|
122 | .. code-block:: c
|
---|
123 |
|
---|
124 | void vk_instance_dispatch_table_from_entrypoints(
|
---|
125 | struct vk_instance_dispatch_table *dispatch_table,
|
---|
126 | const struct vk_instance_entrypoint_table *entrypoint_table,
|
---|
127 | bool overwrite);
|
---|
128 |
|
---|
129 | void vk_physical_device_dispatch_table_from_entrypoints(
|
---|
130 | struct vk_physical_device_dispatch_table *dispatch_table,
|
---|
131 | const struct vk_physical_device_entrypoint_table *entrypoint_table,
|
---|
132 | bool overwrite);
|
---|
133 |
|
---|
134 | void vk_device_dispatch_table_from_entrypoints(
|
---|
135 | struct vk_device_dispatch_table *dispatch_table,
|
---|
136 | const struct vk_device_entrypoint_table *entrypoint_table,
|
---|
137 | bool overwrite);
|
---|
138 |
|
---|
139 |
|
---|
140 | Generating driver dispatch tables
|
---|
141 | ---------------------------------
|
---|
142 |
|
---|
143 | Entrypoint tables can be easily auto-generated for your driver. Simply put
|
---|
144 | the following in the driver's ``meson.build``, modified as necessary:
|
---|
145 |
|
---|
146 | .. code-block::
|
---|
147 |
|
---|
148 | drv_entrypoints = custom_target(
|
---|
149 | 'drv_entrypoints',
|
---|
150 | input : [vk_entrypoints_gen, vk_api_xml],
|
---|
151 | output : ['drv_entrypoints.h', 'drv_entrypoints.c'],
|
---|
152 | command : [
|
---|
153 | prog_python, '@INPUT0@', '--xml', '@INPUT1@', '--proto', '--weak',
|
---|
154 | '--out-h', '@OUTPUT0@', '--out-c', '@OUTPUT1@', '--prefix', 'drv',
|
---|
155 | '--beta', with_vulkan_beta.to_string(),
|
---|
156 | ],
|
---|
157 | depend_files : vk_entrypoints_gen_depend_files,
|
---|
158 | )
|
---|
159 |
|
---|
160 | The generated ``drv_entrypoints.h`` fill will contain prototypes for every
|
---|
161 | Vulkan entrypoint, prefixed with what you passed to ``--prefix`` above.
|
---|
162 | For instance, if you set ``--prefix drv`` and the entrypoint name is
|
---|
163 | ``vkCreateDevice()``, the driver entrypoint will be named
|
---|
164 | ``drv_CreateDevice()``. The ``--prefix`` flag can be specified multiple
|
---|
165 | times if you want more than one table. It also generates an entrypoint
|
---|
166 | table for each prefix and each dispatch level (instance, physical device,
|
---|
167 | and device) which is populated using the driver's functions. Thanks to our
|
---|
168 | use of weak function pointers (or something roughly equivalent for MSVC),
|
---|
169 | any entrypoints which are not implemented will automatically show up as
|
---|
170 | ``NULL`` entries in the table rather than resulting in linking errors.
|
---|
171 |
|
---|
172 | The above generates entrypoint tables because, thanks to aliasing and the C
|
---|
173 | rules around const struct declarations, it's not practical to generate a
|
---|
174 | dispatch table directly. Before they can be passed into the relevant
|
---|
175 | ``vk_*_init()`` function, the entrypoint table will have to be converted to
|
---|
176 | a dispatch table. The typical pattern for this inside a driver looks
|
---|
177 | something like this:
|
---|
178 |
|
---|
179 | .. code-block:: c
|
---|
180 |
|
---|
181 | struct vk_instance_dispatch_table dispatch_table;
|
---|
182 | vk_instance_dispatch_table_from_entrypoints(
|
---|
183 | &dispatch_table, &anv_instance_entrypoints, true);
|
---|
184 | vk_instance_dispatch_table_from_entrypoints(
|
---|
185 | &dispatch_table, &wsi_instance_entrypoints, false);
|
---|
186 |
|
---|
187 | result = vk_instance_init(&instance->vk, &instance_extensions,
|
---|
188 | &dispatch_table, pCreateInfo, pAllocator);
|
---|
189 | if (result != VK_SUCCESS) {
|
---|
190 | vk_free(pAllocator, instance);
|
---|
191 | return result;
|
---|
192 | }
|
---|
193 |
|
---|
194 | The ``vk_*_dispatch_table_from_entrypoints()`` functions are designed so
|
---|
195 | that they can be layered like this. In this case, it starts with the
|
---|
196 | instance entrypoints from the Intel Vulkan driver and then adds in the WSI
|
---|
197 | entrypoints. If there are any entrypoints duplicated between the two, the
|
---|
198 | first one to define the entrypoint wins.
|
---|
199 |
|
---|
200 |
|
---|
201 | Common Vulkan entrypoints
|
---|
202 | -------------------------
|
---|
203 |
|
---|
204 | For the Vulkan runtime itself, there is a dispatch table with the
|
---|
205 | ``vk_common`` prefix used to provide common implementations of various
|
---|
206 | entrypoints. This entrypoint table is added last as part of
|
---|
207 | ``vk_*_init()`` so that the driver implementation will always be used, if
|
---|
208 | there is one.
|
---|
209 |
|
---|
210 | This is used to implement a bunch of things on behalf of the driver. The
|
---|
211 | most common case is whenever there are ``vkFoo()`` and ``vkFoo2()``
|
---|
212 | entrypoints. We provide wrappers for nearly all of these that implement
|
---|
213 | ``vkFoo()`` in terms of ``vkFoo2()`` so a driver can switch to the new one
|
---|
214 | and throw the old one away. For instance, ``vk_common_BindBufferMemory()``
|
---|
215 | looks like this:
|
---|
216 |
|
---|
217 | .. code-block:: c
|
---|
218 |
|
---|
219 | VKAPI_ATTR VkResult VKAPI_CALL
|
---|
220 | vk_common_BindBufferMemory(VkDevice _device,
|
---|
221 | VkBuffer buffer,
|
---|
222 | VkDeviceMemory memory,
|
---|
223 | VkDeviceSize memoryOffset)
|
---|
224 | {
|
---|
225 | VK_FROM_HANDLE(vk_device, device, _device);
|
---|
226 |
|
---|
227 | VkBindBufferMemoryInfo bind = {
|
---|
228 | .sType = VK_STRUCTURE_TYPE_BIND_BUFFER_MEMORY_INFO,
|
---|
229 | .buffer = buffer,
|
---|
230 | .memory = memory,
|
---|
231 | .memoryOffset = memoryOffset,
|
---|
232 | };
|
---|
233 |
|
---|
234 | return device->dispatch_table.BindBufferMemory2(_device, 1, &bind);
|
---|
235 | }
|
---|
236 |
|
---|
237 | There are, of course, far more complicated cases of implementing
|
---|
238 | ``vkFoo()`` in terms of ``vkFoo2()`` such as the
|
---|
239 | ``vk_common_QueueSubmit()`` implementation. We also implement far less
|
---|
240 | trivial functionality as ``vk_common_*`` entrypoints. For instance, we
|
---|
241 | have full implementations of ``VkFence``, ``VkSemaphore``, and
|
---|
242 | ``vkQueueSubmit2()``.
|
---|
243 |
|
---|
244 |
|
---|
245 | Entrypoint lookup
|
---|
246 | -----------------
|
---|
247 |
|
---|
248 | Implementing ``vkGet*ProcAddr()`` is quite complicated because of the
|
---|
249 | Vulkan 1.2 rules around exactly when they have to return ``NULL``. When a
|
---|
250 | client calls ``vkGet*ProcAddr()``, we go through a three step process resolve
|
---|
251 | the function pointer:
|
---|
252 |
|
---|
253 | 1. A static (generated at compile time) hash table is used to map the
|
---|
254 | entrypoint name to an index into the corresponding entry point table.
|
---|
255 |
|
---|
256 | 2. Optionally, the index is passed to an auto-generated function that
|
---|
257 | checks against the enabled core API version and extensions. We use an
|
---|
258 | index into the entrypoint table, not the dispatch table, because the
|
---|
259 | rules for when an entrypoint should be exposed are per-entrypoint. For
|
---|
260 | instance, ``vkBindImageMemory2`` is available on Vulkan 1.1 and later but
|
---|
261 | ``vkBindImageMemory2KHR`` is available if :ext:`VK_KHR_bind_memory2` is
|
---|
262 | enabled.
|
---|
263 |
|
---|
264 | 3. A compaction table is used to map from the entrypoint table index to
|
---|
265 | the dispatch table index and the function is finally fetched from the
|
---|
266 | dispatch table.
|
---|
267 |
|
---|
268 | All of this is encapsulated within the ``vk_*_dispatch_table_get()`` and
|
---|
269 | ``vk_*_dispatch_table_get_if_supported()`` families of functions. The
|
---|
270 | ``_if_supported`` versions take a core version and one or more extension
|
---|
271 | tables. The driver has to provide ``vk_icdGet*ProcAddr()`` entrypoints
|
---|
272 | which wrap these functions because those have to be exposed as actual
|
---|
273 | symbols from the ``.so`` or ``.dll`` as part of the loader interface. It
|
---|
274 | also has to provide its own ``drv_GetInstanceProcAddr()`` because it needs
|
---|
275 | to pass the supported instance extension table to
|
---|
276 | :c:func:`vk_instance_get_proc_addr`. The runtime will provide
|
---|
277 | ``vk_common_GetDeviceProcAddr()`` implementations.
|
---|
278 |
|
---|
279 |
|
---|
280 | Populating layer or client dispatch tables
|
---|
281 | ------------------------------------------
|
---|
282 |
|
---|
283 | The entrypoint and dispatch tables actually live in ``src/vulkan/util``,
|
---|
284 | not ``src/vulkan/runtime`` so they can be used by layers and clients (such
|
---|
285 | as Zink) as well as the runtime. Layers and clients may wish to populate
|
---|
286 | dispatch tables from an underlying Vulkan implementation. This can be done
|
---|
287 | via the ``vk_*_dispatch_table_load()`` family of functions:
|
---|
288 |
|
---|
289 | .. code-block:: c
|
---|
290 |
|
---|
291 | void
|
---|
292 | vk_instance_dispatch_table_load(struct vk_instance_dispatch_table *table,
|
---|
293 | PFN_vkGetInstanceProcAddr gpa,
|
---|
294 | VkInstance instance);
|
---|
295 | void
|
---|
296 | vk_physical_device_dispatch_table_load(struct vk_physical_device_dispatch_table *table,
|
---|
297 | PFN_vkGetInstanceProcAddr gpa,
|
---|
298 | VkInstance instance);
|
---|
299 | void
|
---|
300 | vk_device_dispatch_table_load(struct vk_device_dispatch_table *table,
|
---|
301 | PFN_vkGetDeviceProcAddr gpa,
|
---|
302 | VkDevice device);
|
---|
303 |
|
---|
304 | These call the given ``vkGet*ProcAddr`` function to populate the dispatch
|
---|
305 | table. For aliased entrypoints, it will try each variant in succession to
|
---|
306 | ensure that the dispatch table entry gets populated no matter which version
|
---|
307 | of the feature you have enabled.
|
---|