├── .github
└── FUNDING.yml
├── .gitmodules
├── LICENSE
├── README.md
├── assets
├── icons
│ ├── add.png
│ ├── arrow_back.png
│ ├── check_circle.png
│ ├── close.png
│ ├── delete.png
│ ├── favorite.png
│ ├── home.png
│ ├── logout.png
│ ├── menu.png
│ ├── search.png
│ ├── settings.png
│ └── star.png
├── screenshot.png
├── screenshot_fedora_gnome.png
└── screenshot_void_sway.png
├── build.zig
├── deps
├── vk.xml
└── wayland-protocols
│ ├── stable
│ └── xdg-shell
│ │ └── xdg-shell.xml
│ └── unstable
│ └── xdg-decoration
│ └── xdg-decoration-unstable-v1.xml
├── shaders
├── compile.sh
├── generic.frag
├── generic.frag.spv
├── generic.vert
├── generic.vert.spv
└── shaders.zig
└── src
├── main.zig
└── vulkan_config.zig
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13 | custom: ['paypal.me/fairytreesoftware']
14 |
15 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "deps/zigimg"]
2 | path = deps/zigimg
3 | url = https://github.com/zigimg/zigimg
4 | [submodule "deps/vulkan-zig"]
5 | path = deps/vulkan-zig
6 | url = https://github.com/Snektron/vulkan-zig
7 | [submodule "deps/zig-wayland"]
8 | path = deps/zig-wayland
9 | url = https://github.com/kdchambers/zig-wayland
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Keith Chambers
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # vkwayland
2 |
3 | *A reference application for vulkan and wayland.*
4 |
5 | 
6 |
7 | ### Goals
8 |
9 | - Easy to read and understand the code (Avoiding heavy abstractions or unrelated cruft)
10 | - Make use of typical vulkan / wayland functionality
11 | - Be performant and correct
12 | - A common place to iron out best practices
13 |
14 | I'm not an expert in vulkan or wayland, so audits are welcome.
15 |
16 | Additionally, feature requests, questions and discussions are in scope for the project. Feel free to open an issue around a topic.
17 |
18 | ### Features
19 |
20 | - Mostly self-contained within a ~3k loc source file
21 | - Animated background (Color smoothly updates each frame)
22 | - [Client-side](https://en.wikipedia.org/wiki/Client-side_decoration) window decorations for compositors that don't support drawing them on behalf of application (E.g Gnome / mutter)
23 | - Updates cursor icon when application surface is entered
24 | - Vulkan specfic wayland integration (Not using waylands shared memory buffer interface)
25 | - Proper (mostly) querying of vulkan objects (Devices, memory, etc)
26 | - Vulkan synchonization that doesn't rely on deviceWaitIdle (Except on shutdown)
27 | - Dynamic viewport + scissor for more efficient swapchain recreation
28 | - Image loading and texture sampling
29 | - Surface transparency
30 | - Window movement and resizing
31 |
32 | ## Requirements
33 |
34 | - [zig](https://github.com/ziglang/zig) (Tested with 0.12.0-dev.3180)
35 | - Wayland system (mutter, river, sway, etc)
36 | - libwayland-client
37 | - libwayland-cursor
38 | - vulkan validation layers (Optional, only in Debug mode)
39 |
40 | ## Running
41 |
42 | git clone --recurse-submodules https://github.com/kdchambers/vkwayland
43 | cd vkwayland
44 | zig build run -Drelease-safe
45 |
46 | **NOTE**: If you run the application in debug mode, you will get an error if vulkan validation layers aren't installed on your system.
47 |
48 | ## Credits
49 |
50 | This application makes use of the following libraries and credits go to the authers and contributors for allowing this project to rely soley on zig code.
51 |
52 | - [zigimg](https://github.com/zigimg/zigimg)
53 | - [zig-vulkan](https://github.com/Snektron/vulkan-zig)
54 | - [zig-wayland](https://github.com/ifreund/zig-wayland)
55 |
56 | ## License
57 |
58 | MIT
59 |
--------------------------------------------------------------------------------
/assets/icons/add.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kdchambers/vkwayland/266af65c0a9ef97f6218878cfa78d9f176be9d04/assets/icons/add.png
--------------------------------------------------------------------------------
/assets/icons/arrow_back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kdchambers/vkwayland/266af65c0a9ef97f6218878cfa78d9f176be9d04/assets/icons/arrow_back.png
--------------------------------------------------------------------------------
/assets/icons/check_circle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kdchambers/vkwayland/266af65c0a9ef97f6218878cfa78d9f176be9d04/assets/icons/check_circle.png
--------------------------------------------------------------------------------
/assets/icons/close.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kdchambers/vkwayland/266af65c0a9ef97f6218878cfa78d9f176be9d04/assets/icons/close.png
--------------------------------------------------------------------------------
/assets/icons/delete.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kdchambers/vkwayland/266af65c0a9ef97f6218878cfa78d9f176be9d04/assets/icons/delete.png
--------------------------------------------------------------------------------
/assets/icons/favorite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kdchambers/vkwayland/266af65c0a9ef97f6218878cfa78d9f176be9d04/assets/icons/favorite.png
--------------------------------------------------------------------------------
/assets/icons/home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kdchambers/vkwayland/266af65c0a9ef97f6218878cfa78d9f176be9d04/assets/icons/home.png
--------------------------------------------------------------------------------
/assets/icons/logout.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kdchambers/vkwayland/266af65c0a9ef97f6218878cfa78d9f176be9d04/assets/icons/logout.png
--------------------------------------------------------------------------------
/assets/icons/menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kdchambers/vkwayland/266af65c0a9ef97f6218878cfa78d9f176be9d04/assets/icons/menu.png
--------------------------------------------------------------------------------
/assets/icons/search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kdchambers/vkwayland/266af65c0a9ef97f6218878cfa78d9f176be9d04/assets/icons/search.png
--------------------------------------------------------------------------------
/assets/icons/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kdchambers/vkwayland/266af65c0a9ef97f6218878cfa78d9f176be9d04/assets/icons/settings.png
--------------------------------------------------------------------------------
/assets/icons/star.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kdchambers/vkwayland/266af65c0a9ef97f6218878cfa78d9f176be9d04/assets/icons/star.png
--------------------------------------------------------------------------------
/assets/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kdchambers/vkwayland/266af65c0a9ef97f6218878cfa78d9f176be9d04/assets/screenshot.png
--------------------------------------------------------------------------------
/assets/screenshot_fedora_gnome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kdchambers/vkwayland/266af65c0a9ef97f6218878cfa78d9f176be9d04/assets/screenshot_fedora_gnome.png
--------------------------------------------------------------------------------
/assets/screenshot_void_sway.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kdchambers/vkwayland/266af65c0a9ef97f6218878cfa78d9f176be9d04/assets/screenshot_void_sway.png
--------------------------------------------------------------------------------
/build.zig:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | // Copyright (c) 2024 Keith Chambers
3 |
4 | const std = @import("std");
5 |
6 | const Build = std.Build;
7 | const Pkg = Build.Pkg;
8 |
9 | const vkgen = @import("deps/vulkan-zig/generator/index.zig");
10 | const Scanner = @import("deps/zig-wayland/build.zig").Scanner;
11 |
12 | pub fn build(b: *Build) !void {
13 | const target = b.standardTargetOptions(.{});
14 | const optimize = b.standardOptimizeOption(.{});
15 |
16 | const scanner = try Scanner.create(b, .{
17 | // .wayland_xml_path = "/usr/share/wayland/wayland.xml",
18 | // .wayland_protocols_path = "/usr/share/wayland-protocols",
19 | .target = target,
20 | });
21 | const wayland_module = b.createModule(.{ .root_source_file = scanner.result });
22 |
23 | scanner.addCustomProtocol("deps/wayland-protocols/stable/xdg-shell/xdg-shell.xml");
24 | scanner.addCustomProtocol("deps/wayland-protocols/unstable/xdg-decoration/xdg-decoration-unstable-v1.xml");
25 |
26 | scanner.generate("xdg_wm_base", 2);
27 | scanner.generate("wl_compositor", 4);
28 | scanner.generate("wl_seat", 5);
29 | scanner.generate("wl_shm", 1);
30 |
31 | scanner.generate("zxdg_decoration_manager_v1", 1);
32 |
33 | const exe = b.addExecutable(.{
34 | .name = "vkwayland",
35 | .root_source_file = .{ .path = "src/main.zig" },
36 | .target = target,
37 | .optimize = optimize,
38 | });
39 |
40 | const shader_module = b.createModule(.{
41 | .root_source_file = .{ .path = "shaders/shaders.zig" },
42 | });
43 |
44 | const zigimg_module = b.createModule(.{
45 | .root_source_file = .{ .path = "deps/zigimg/zigimg.zig" },
46 | });
47 |
48 | const gen = vkgen.VkGenerateStep.create(b, "deps/vk.xml");
49 |
50 | const vulkan_module = b.createModule(.{
51 | .root_source_file = gen.getSource(),
52 | });
53 |
54 | exe.root_module.addImport("shaders", shader_module);
55 | exe.root_module.addImport("zigimg", zigimg_module);
56 | exe.root_module.addImport("vulkan", vulkan_module);
57 | exe.root_module.addImport("wayland", wayland_module);
58 |
59 | exe.linkLibC();
60 | exe.linkSystemLibrary("wayland-client");
61 | exe.linkSystemLibrary("wayland-cursor");
62 |
63 | // NOTE: Taken from https://github.com/ifreund/hello-zig-wayland/blob/master/build.zig
64 | // TODO: remove when https://github.com/ziglang/zig/issues/131 is implemented
65 | scanner.addCSource(exe);
66 |
67 | b.installArtifact(exe);
68 |
69 | const run_cmd = b.addRunArtifact(exe);
70 | if (b.args) |args| run_cmd.addArgs(args);
71 | run_cmd.step.dependOn(b.getInstallStep());
72 |
73 | const run_step = b.step("run", "Run vkwayland");
74 | run_step.dependOn(&run_cmd.step);
75 | }
76 |
--------------------------------------------------------------------------------
/deps/wayland-protocols/stable/xdg-shell/xdg-shell.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Copyright © 2008-2013 Kristian Høgsberg
6 | Copyright © 2013 Rafael Antognolli
7 | Copyright © 2013 Jasper St. Pierre
8 | Copyright © 2010-2013 Intel Corporation
9 | Copyright © 2015-2017 Samsung Electronics Co., Ltd
10 | Copyright © 2015-2017 Red Hat Inc.
11 |
12 | Permission is hereby granted, free of charge, to any person obtaining a
13 | copy of this software and associated documentation files (the "Software"),
14 | to deal in the Software without restriction, including without limitation
15 | the rights to use, copy, modify, merge, publish, distribute, sublicense,
16 | and/or sell copies of the Software, and to permit persons to whom the
17 | Software is furnished to do so, subject to the following conditions:
18 |
19 | The above copyright notice and this permission notice (including the next
20 | paragraph) shall be included in all copies or substantial portions of the
21 | Software.
22 |
23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
26 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
28 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
29 | DEALINGS IN THE SOFTWARE.
30 |
31 |
32 |
33 |
34 | The xdg_wm_base interface is exposed as a global object enabling clients
35 | to turn their wl_surfaces into windows in a desktop environment. It
36 | defines the basic functionality needed for clients and the compositor to
37 | create windows that can be dragged, resized, maximized, etc, as well as
38 | creating transient windows such as popup menus.
39 |
40 |
41 |
42 |
43 |
45 |
47 |
49 |
51 |
53 |
54 |
55 |
56 |
57 | Destroy this xdg_wm_base object.
58 |
59 | Destroying a bound xdg_wm_base object while there are surfaces
60 | still alive created by this xdg_wm_base object instance is illegal
61 | and will result in a protocol error.
62 |
63 |
64 |
65 |
66 |
67 | Create a positioner object. A positioner object is used to position
68 | surfaces relative to some parent surface. See the interface description
69 | and xdg_surface.get_popup for details.
70 |
71 |
72 |
73 |
74 |
75 |
76 | This creates an xdg_surface for the given surface. While xdg_surface
77 | itself is not a role, the corresponding surface may only be assigned
78 | a role extending xdg_surface, such as xdg_toplevel or xdg_popup. It is
79 | illegal to create an xdg_surface for a wl_surface which already has an
80 | assigned role and this will result in a protocol error.
81 |
82 | This creates an xdg_surface for the given surface. An xdg_surface is
83 | used as basis to define a role to a given surface, such as xdg_toplevel
84 | or xdg_popup. It also manages functionality shared between xdg_surface
85 | based surface roles.
86 |
87 | See the documentation of xdg_surface for more details about what an
88 | xdg_surface is and how it is used.
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 | A client must respond to a ping event with a pong request or
97 | the client may be deemed unresponsive. See xdg_wm_base.ping.
98 |
99 |
100 |
101 |
102 |
103 |
104 | The ping event asks the client if it's still alive. Pass the
105 | serial specified in the event back to the compositor by sending
106 | a "pong" request back with the specified serial. See xdg_wm_base.pong.
107 |
108 | Compositors can use this to determine if the client is still
109 | alive. It's unspecified what will happen if the client doesn't
110 | respond to the ping request, or in what timeframe. Clients should
111 | try to respond in a reasonable amount of time.
112 |
113 | A compositor is free to ping in any way it wants, but a client must
114 | always respond to any xdg_wm_base object it created.
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 | The xdg_positioner provides a collection of rules for the placement of a
123 | child surface relative to a parent surface. Rules can be defined to ensure
124 | the child surface remains within the visible area's borders, and to
125 | specify how the child surface changes its position, such as sliding along
126 | an axis, or flipping around a rectangle. These positioner-created rules are
127 | constrained by the requirement that a child surface must intersect with or
128 | be at least partially adjacent to its parent surface.
129 |
130 | See the various requests for details about possible rules.
131 |
132 | At the time of the request, the compositor makes a copy of the rules
133 | specified by the xdg_positioner. Thus, after the request is complete the
134 | xdg_positioner object can be destroyed or reused; further changes to the
135 | object will have no effect on previous usages.
136 |
137 | For an xdg_positioner object to be considered complete, it must have a
138 | non-zero size set by set_size, and a non-zero anchor rectangle set by
139 | set_anchor_rect. Passing an incomplete xdg_positioner object when
140 | positioning a surface raises an error.
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 | Notify the compositor that the xdg_positioner will no longer be used.
150 |
151 |
152 |
153 |
154 |
155 | Set the size of the surface that is to be positioned with the positioner
156 | object. The size is in surface-local coordinates and corresponds to the
157 | window geometry. See xdg_surface.set_window_geometry.
158 |
159 | If a zero or negative size is set the invalid_input error is raised.
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 | Specify the anchor rectangle within the parent surface that the child
168 | surface will be placed relative to. The rectangle is relative to the
169 | window geometry as defined by xdg_surface.set_window_geometry of the
170 | parent surface.
171 |
172 | When the xdg_positioner object is used to position a child surface, the
173 | anchor rectangle may not extend outside the window geometry of the
174 | positioned child's parent surface.
175 |
176 | If a negative size is set the invalid_input error is raised.
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 | Defines the anchor point for the anchor rectangle. The specified anchor
199 | is used derive an anchor point that the child surface will be
200 | positioned relative to. If a corner anchor is set (e.g. 'top_left' or
201 | 'bottom_right'), the anchor point will be at the specified corner;
202 | otherwise, the derived anchor point will be centered on the specified
203 | edge, or in the center of the anchor rectangle if no edge is specified.
204 |
205 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 | Defines in what direction a surface should be positioned, relative to
224 | the anchor point of the parent surface. If a corner gravity is
225 | specified (e.g. 'bottom_right' or 'top_left'), then the child surface
226 | will be placed towards the specified gravity; otherwise, the child
227 | surface will be centered over the anchor point on any axis that had no
228 | gravity specified.
229 |
230 |
232 |
233 |
234 |
235 |
236 | The constraint adjustment value define ways the compositor will adjust
237 | the position of the surface, if the unadjusted position would result
238 | in the surface being partly constrained.
239 |
240 | Whether a surface is considered 'constrained' is left to the compositor
241 | to determine. For example, the surface may be partly outside the
242 | compositor's defined 'work area', thus necessitating the child surface's
243 | position be adjusted until it is entirely inside the work area.
244 |
245 | The adjustments can be combined, according to a defined precedence: 1)
246 | Flip, 2) Slide, 3) Resize.
247 |
248 |
249 |
250 | Don't alter the surface position even if it is constrained on some
251 | axis, for example partially outside the edge of an output.
252 |
253 |
254 |
255 |
256 | Slide the surface along the x axis until it is no longer constrained.
257 |
258 | First try to slide towards the direction of the gravity on the x axis
259 | until either the edge in the opposite direction of the gravity is
260 | unconstrained or the edge in the direction of the gravity is
261 | constrained.
262 |
263 | Then try to slide towards the opposite direction of the gravity on the
264 | x axis until either the edge in the direction of the gravity is
265 | unconstrained or the edge in the opposite direction of the gravity is
266 | constrained.
267 |
268 |
269 |
270 |
271 | Slide the surface along the y axis until it is no longer constrained.
272 |
273 | First try to slide towards the direction of the gravity on the y axis
274 | until either the edge in the opposite direction of the gravity is
275 | unconstrained or the edge in the direction of the gravity is
276 | constrained.
277 |
278 | Then try to slide towards the opposite direction of the gravity on the
279 | y axis until either the edge in the direction of the gravity is
280 | unconstrained or the edge in the opposite direction of the gravity is
281 | constrained.
282 |
283 |
284 |
285 |
286 | Invert the anchor and gravity on the x axis if the surface is
287 | constrained on the x axis. For example, if the left edge of the
288 | surface is constrained, the gravity is 'left' and the anchor is
289 | 'left', change the gravity to 'right' and the anchor to 'right'.
290 |
291 | If the adjusted position also ends up being constrained, the resulting
292 | position of the flip_x adjustment will be the one before the
293 | adjustment.
294 |
295 |
296 |
297 |
298 | Invert the anchor and gravity on the y axis if the surface is
299 | constrained on the y axis. For example, if the bottom edge of the
300 | surface is constrained, the gravity is 'bottom' and the anchor is
301 | 'bottom', change the gravity to 'top' and the anchor to 'top'.
302 |
303 | The adjusted position is calculated given the original anchor
304 | rectangle and offset, but with the new flipped anchor and gravity
305 | values.
306 |
307 | If the adjusted position also ends up being constrained, the resulting
308 | position of the flip_y adjustment will be the one before the
309 | adjustment.
310 |
311 |
312 |
313 |
314 | Resize the surface horizontally so that it is completely
315 | unconstrained.
316 |
317 |
318 |
319 |
320 | Resize the surface vertically so that it is completely unconstrained.
321 |
322 |
323 |
324 |
325 |
326 |
327 | Specify how the window should be positioned if the originally intended
328 | position caused the surface to be constrained, meaning at least
329 | partially outside positioning boundaries set by the compositor. The
330 | adjustment is set by constructing a bitmask describing the adjustment to
331 | be made when the surface is constrained on that axis.
332 |
333 | If no bit for one axis is set, the compositor will assume that the child
334 | surface should not change its position on that axis when constrained.
335 |
336 | If more than one bit for one axis is set, the order of how adjustments
337 | are applied is specified in the corresponding adjustment descriptions.
338 |
339 | The default adjustment is none.
340 |
341 |
343 |
344 |
345 |
346 |
347 | Specify the surface position offset relative to the position of the
348 | anchor on the anchor rectangle and the anchor on the surface. For
349 | example if the anchor of the anchor rectangle is at (x, y), the surface
350 | has the gravity bottom|right, and the offset is (ox, oy), the calculated
351 | surface position will be (x + ox, y + oy). The offset position of the
352 | surface is the one used for constraint testing. See
353 | set_constraint_adjustment.
354 |
355 | An example use case is placing a popup menu on top of a user interface
356 | element, while aligning the user interface element of the parent surface
357 | with some user interface element placed somewhere in the popup surface.
358 |
359 |
360 |
361 |
362 |
363 |
364 |
365 |
366 |
367 | When set reactive, the surface is reconstrained if the conditions used
368 | for constraining changed, e.g. the parent window moved.
369 |
370 | If the conditions changed and the popup was reconstrained, an
371 | xdg_popup.configure event is sent with updated geometry, followed by an
372 | xdg_surface.configure event.
373 |
374 |
375 |
376 |
377 |
378 | Set the parent window geometry the compositor should use when
379 | positioning the popup. The compositor may use this information to
380 | determine the future state the popup should be constrained using. If
381 | this doesn't match the dimension of the parent the popup is eventually
382 | positioned against, the behavior is undefined.
383 |
384 | The arguments are given in the surface-local coordinate space.
385 |
386 |
388 |
390 |
391 |
392 |
393 |
394 | Set the serial of an xdg_surface.configure event this positioner will be
395 | used in response to. The compositor may use this information together
396 | with set_parent_size to determine what future state the popup should be
397 | constrained using.
398 |
399 |
401 |
402 |
403 |
404 |
405 |
406 | An interface that may be implemented by a wl_surface, for
407 | implementations that provide a desktop-style user interface.
408 |
409 | It provides a base set of functionality required to construct user
410 | interface elements requiring management by the compositor, such as
411 | toplevel windows, menus, etc. The types of functionality are split into
412 | xdg_surface roles.
413 |
414 | Creating an xdg_surface does not set the role for a wl_surface. In order
415 | to map an xdg_surface, the client must create a role-specific object
416 | using, e.g., get_toplevel, get_popup. The wl_surface for any given
417 | xdg_surface can have at most one role, and may not be assigned any role
418 | not based on xdg_surface.
419 |
420 | A role must be assigned before any other requests are made to the
421 | xdg_surface object.
422 |
423 | The client must call wl_surface.commit on the corresponding wl_surface
424 | for the xdg_surface state to take effect.
425 |
426 | Creating an xdg_surface from a wl_surface which has a buffer attached or
427 | committed is a client error, and any attempts by a client to attach or
428 | manipulate a buffer prior to the first xdg_surface.configure call must
429 | also be treated as errors.
430 |
431 | After creating a role-specific object and setting it up, the client must
432 | perform an initial commit without any buffer attached. The compositor
433 | will reply with an xdg_surface.configure event. The client must
434 | acknowledge it and is then allowed to attach a buffer to map the surface.
435 |
436 | Mapping an xdg_surface-based role surface is defined as making it
437 | possible for the surface to be shown by the compositor. Note that
438 | a mapped surface is not guaranteed to be visible once it is mapped.
439 |
440 | For an xdg_surface to be mapped by the compositor, the following
441 | conditions must be met:
442 | (1) the client has assigned an xdg_surface-based role to the surface
443 | (2) the client has set and committed the xdg_surface state and the
444 | role-dependent state to the surface
445 | (3) the client has committed a buffer to the surface
446 |
447 | A newly-unmapped surface is considered to have met condition (1) out
448 | of the 3 required conditions for mapping a surface if its role surface
449 | has not been destroyed, i.e. the client must perform the initial commit
450 | again before attaching a buffer.
451 |
452 |
453 |
454 |
455 |
456 |
457 |
458 |
459 |
460 |
461 | Destroy the xdg_surface object. An xdg_surface must only be destroyed
462 | after its role object has been destroyed.
463 |
464 |
465 |
466 |
467 |
468 | This creates an xdg_toplevel object for the given xdg_surface and gives
469 | the associated wl_surface the xdg_toplevel role.
470 |
471 | See the documentation of xdg_toplevel for more details about what an
472 | xdg_toplevel is and how it is used.
473 |
474 |
475 |
476 |
477 |
478 |
479 | This creates an xdg_popup object for the given xdg_surface and gives
480 | the associated wl_surface the xdg_popup role.
481 |
482 | If null is passed as a parent, a parent surface must be specified using
483 | some other protocol, before committing the initial state.
484 |
485 | See the documentation of xdg_popup for more details about what an
486 | xdg_popup is and how it is used.
487 |
488 |
489 |
490 |
491 |
492 |
493 |
494 |
495 | The window geometry of a surface is its "visible bounds" from the
496 | user's perspective. Client-side decorations often have invisible
497 | portions like drop-shadows which should be ignored for the
498 | purposes of aligning, placing and constraining windows.
499 |
500 | The window geometry is double buffered, and will be applied at the
501 | time wl_surface.commit of the corresponding wl_surface is called.
502 |
503 | When maintaining a position, the compositor should treat the (x, y)
504 | coordinate of the window geometry as the top left corner of the window.
505 | A client changing the (x, y) window geometry coordinate should in
506 | general not alter the position of the window.
507 |
508 | Once the window geometry of the surface is set, it is not possible to
509 | unset it, and it will remain the same until set_window_geometry is
510 | called again, even if a new subsurface or buffer is attached.
511 |
512 | If never set, the value is the full bounds of the surface,
513 | including any subsurfaces. This updates dynamically on every
514 | commit. This unset is meant for extremely simple clients.
515 |
516 | The arguments are given in the surface-local coordinate space of
517 | the wl_surface associated with this xdg_surface.
518 |
519 | The width and height must be greater than zero. Setting an invalid size
520 | will raise an error. When applied, the effective window geometry will be
521 | the set window geometry clamped to the bounding rectangle of the
522 | combined geometry of the surface of the xdg_surface and the associated
523 | subsurfaces.
524 |
525 |
526 |
527 |
528 |
529 |
530 |
531 |
532 |
533 | When a configure event is received, if a client commits the
534 | surface in response to the configure event, then the client
535 | must make an ack_configure request sometime before the commit
536 | request, passing along the serial of the configure event.
537 |
538 | For instance, for toplevel surfaces the compositor might use this
539 | information to move a surface to the top left only when the client has
540 | drawn itself for the maximized or fullscreen state.
541 |
542 | If the client receives multiple configure events before it
543 | can respond to one, it only has to ack the last configure event.
544 |
545 | A client is not required to commit immediately after sending
546 | an ack_configure request - it may even ack_configure several times
547 | before its next surface commit.
548 |
549 | A client may send multiple ack_configure requests before committing, but
550 | only the last request sent before a commit indicates which configure
551 | event the client really is responding to.
552 |
553 |
554 |
555 |
556 |
557 |
558 | The configure event marks the end of a configure sequence. A configure
559 | sequence is a set of one or more events configuring the state of the
560 | xdg_surface, including the final xdg_surface.configure event.
561 |
562 | Where applicable, xdg_surface surface roles will during a configure
563 | sequence extend this event as a latched state sent as events before the
564 | xdg_surface.configure event. Such events should be considered to make up
565 | a set of atomically applied configuration states, where the
566 | xdg_surface.configure commits the accumulated state.
567 |
568 | Clients should arrange their surface for the new states, and then send
569 | an ack_configure request with the serial sent in this configure event at
570 | some point before committing the new surface.
571 |
572 | If the client receives multiple configure events before it can respond
573 | to one, it is free to discard all but the last event it received.
574 |
575 |
576 |
577 |
578 |
579 |
580 |
581 |
582 | This interface defines an xdg_surface role which allows a surface to,
583 | among other things, set window-like properties such as maximize,
584 | fullscreen, and minimize, set application-specific metadata like title and
585 | id, and well as trigger user interactive operations such as interactive
586 | resize and move.
587 |
588 | Unmapping an xdg_toplevel means that the surface cannot be shown
589 | by the compositor until it is explicitly mapped again.
590 | All active operations (e.g., move, resize) are canceled and all
591 | attributes (e.g. title, state, stacking, ...) are discarded for
592 | an xdg_toplevel surface when it is unmapped. The xdg_toplevel returns to
593 | the state it had right after xdg_surface.get_toplevel. The client
594 | can re-map the toplevel by perfoming a commit without any buffer
595 | attached, waiting for a configure event and handling it as usual (see
596 | xdg_surface description).
597 |
598 | Attaching a null buffer to a toplevel unmaps the surface.
599 |
600 |
601 |
602 |
603 | This request destroys the role surface and unmaps the surface;
604 | see "Unmapping" behavior in interface section for details.
605 |
606 |
607 |
608 |
609 |
611 |
612 |
613 |
614 |
615 | Set the "parent" of this surface. This surface should be stacked
616 | above the parent surface and all other ancestor surfaces.
617 |
618 | Parent surfaces should be set on dialogs, toolboxes, or other
619 | "auxiliary" surfaces, so that the parent is raised when the dialog
620 | is raised.
621 |
622 | Setting a null parent for a child surface unsets its parent. Setting
623 | a null parent for a surface which currently has no parent is a no-op.
624 |
625 | Only mapped surfaces can have child surfaces. Setting a parent which
626 | is not mapped is equivalent to setting a null parent. If a surface
627 | becomes unmapped, its children's parent is set to the parent of
628 | the now-unmapped surface. If the now-unmapped surface has no parent,
629 | its children's parent is unset. If the now-unmapped surface becomes
630 | mapped again, its parent-child relationship is not restored.
631 |
632 |
633 |
634 |
635 |
636 |
637 | Set a short title for the surface.
638 |
639 | This string may be used to identify the surface in a task bar,
640 | window list, or other user interface elements provided by the
641 | compositor.
642 |
643 | The string must be encoded in UTF-8.
644 |
645 |
646 |
647 |
648 |
649 |
650 | Set an application identifier for the surface.
651 |
652 | The app ID identifies the general class of applications to which
653 | the surface belongs. The compositor can use this to group multiple
654 | surfaces together, or to determine how to launch a new application.
655 |
656 | For D-Bus activatable applications, the app ID is used as the D-Bus
657 | service name.
658 |
659 | The compositor shell will try to group application surfaces together
660 | by their app ID. As a best practice, it is suggested to select app
661 | ID's that match the basename of the application's .desktop file.
662 | For example, "org.freedesktop.FooViewer" where the .desktop file is
663 | "org.freedesktop.FooViewer.desktop".
664 |
665 | Like other properties, a set_app_id request can be sent after the
666 | xdg_toplevel has been mapped to update the property.
667 |
668 | See the desktop-entry specification [0] for more details on
669 | application identifiers and how they relate to well-known D-Bus
670 | names and .desktop files.
671 |
672 | [0] http://standards.freedesktop.org/desktop-entry-spec/
673 |
674 |
675 |
676 |
677 |
678 |
679 | Clients implementing client-side decorations might want to show
680 | a context menu when right-clicking on the decorations, giving the
681 | user a menu that they can use to maximize or minimize the window.
682 |
683 | This request asks the compositor to pop up such a window menu at
684 | the given position, relative to the local surface coordinates of
685 | the parent surface. There are no guarantees as to what menu items
686 | the window menu contains.
687 |
688 | This request must be used in response to some sort of user action
689 | like a button press, key press, or touch down event.
690 |
691 |
692 |
693 |
694 |
695 |
696 |
697 |
698 |
699 | Start an interactive, user-driven move of the surface.
700 |
701 | This request must be used in response to some sort of user action
702 | like a button press, key press, or touch down event. The passed
703 | serial is used to determine the type of interactive move (touch,
704 | pointer, etc).
705 |
706 | The server may ignore move requests depending on the state of
707 | the surface (e.g. fullscreen or maximized), or if the passed serial
708 | is no longer valid.
709 |
710 | If triggered, the surface will lose the focus of the device
711 | (wl_pointer, wl_touch, etc) used for the move. It is up to the
712 | compositor to visually indicate that the move is taking place, such as
713 | updating a pointer cursor, during the move. There is no guarantee
714 | that the device focus will return when the move is completed.
715 |
716 |
717 |
718 |
719 |
720 |
721 |
722 | These values are used to indicate which edge of a surface
723 | is being dragged in a resize operation.
724 |
725 |
726 |
727 |
728 |
729 |
730 |
731 |
732 |
733 |
734 |
735 |
736 |
737 |
738 | Start a user-driven, interactive resize of the surface.
739 |
740 | This request must be used in response to some sort of user action
741 | like a button press, key press, or touch down event. The passed
742 | serial is used to determine the type of interactive resize (touch,
743 | pointer, etc).
744 |
745 | The server may ignore resize requests depending on the state of
746 | the surface (e.g. fullscreen or maximized).
747 |
748 | If triggered, the client will receive configure events with the
749 | "resize" state enum value and the expected sizes. See the "resize"
750 | enum value for more details about what is required. The client
751 | must also acknowledge configure events using "ack_configure". After
752 | the resize is completed, the client will receive another "configure"
753 | event without the resize state.
754 |
755 | If triggered, the surface also will lose the focus of the device
756 | (wl_pointer, wl_touch, etc) used for the resize. It is up to the
757 | compositor to visually indicate that the resize is taking place,
758 | such as updating a pointer cursor, during the resize. There is no
759 | guarantee that the device focus will return when the resize is
760 | completed.
761 |
762 | The edges parameter specifies how the surface should be resized, and
763 | is one of the values of the resize_edge enum. Values not matching
764 | a variant of the enum will cause a protocol error. The compositor
765 | may use this information to update the surface position for example
766 | when dragging the top left corner. The compositor may also use
767 | this information to adapt its behavior, e.g. choose an appropriate
768 | cursor image.
769 |
770 |
771 |
772 |
773 |
774 |
775 |
776 |
777 | The different state values used on the surface. This is designed for
778 | state values like maximized, fullscreen. It is paired with the
779 | configure event to ensure that both the client and the compositor
780 | setting the state can be synchronized.
781 |
782 | States set in this way are double-buffered. They will get applied on
783 | the next commit.
784 |
785 |
786 |
787 | The surface is maximized. The window geometry specified in the configure
788 | event must be obeyed by the client.
789 |
790 | The client should draw without shadow or other
791 | decoration outside of the window geometry.
792 |
793 |
794 |
795 |
796 | The surface is fullscreen. The window geometry specified in the
797 | configure event is a maximum; the client cannot resize beyond it. For
798 | a surface to cover the whole fullscreened area, the geometry
799 | dimensions must be obeyed by the client. For more details, see
800 | xdg_toplevel.set_fullscreen.
801 |
802 |
803 |
804 |
805 | The surface is being resized. The window geometry specified in the
806 | configure event is a maximum; the client cannot resize beyond it.
807 | Clients that have aspect ratio or cell sizing configuration can use
808 | a smaller size, however.
809 |
810 |
811 |
812 |
813 | Client window decorations should be painted as if the window is
814 | active. Do not assume this means that the window actually has
815 | keyboard or pointer focus.
816 |
817 |
818 |
819 |
820 | The window is currently in a tiled layout and the left edge is
821 | considered to be adjacent to another part of the tiling grid.
822 |
823 |
824 |
825 |
826 | The window is currently in a tiled layout and the right edge is
827 | considered to be adjacent to another part of the tiling grid.
828 |
829 |
830 |
831 |
832 | The window is currently in a tiled layout and the top edge is
833 | considered to be adjacent to another part of the tiling grid.
834 |
835 |
836 |
837 |
838 | The window is currently in a tiled layout and the bottom edge is
839 | considered to be adjacent to another part of the tiling grid.
840 |
841 |
842 |
843 |
844 |
845 |
846 | Set a maximum size for the window.
847 |
848 | The client can specify a maximum size so that the compositor does
849 | not try to configure the window beyond this size.
850 |
851 | The width and height arguments are in window geometry coordinates.
852 | See xdg_surface.set_window_geometry.
853 |
854 | Values set in this way are double-buffered. They will get applied
855 | on the next commit.
856 |
857 | The compositor can use this information to allow or disallow
858 | different states like maximize or fullscreen and draw accurate
859 | animations.
860 |
861 | Similarly, a tiling window manager may use this information to
862 | place and resize client windows in a more effective way.
863 |
864 | The client should not rely on the compositor to obey the maximum
865 | size. The compositor may decide to ignore the values set by the
866 | client and request a larger size.
867 |
868 | If never set, or a value of zero in the request, means that the
869 | client has no expected maximum size in the given dimension.
870 | As a result, a client wishing to reset the maximum size
871 | to an unspecified state can use zero for width and height in the
872 | request.
873 |
874 | Requesting a maximum size to be smaller than the minimum size of
875 | a surface is illegal and will result in a protocol error.
876 |
877 | The width and height must be greater than or equal to zero. Using
878 | strictly negative values for width and height will result in a
879 | protocol error.
880 |
881 |
882 |
883 |
884 |
885 |
886 |
887 | Set a minimum size for the window.
888 |
889 | The client can specify a minimum size so that the compositor does
890 | not try to configure the window below this size.
891 |
892 | The width and height arguments are in window geometry coordinates.
893 | See xdg_surface.set_window_geometry.
894 |
895 | Values set in this way are double-buffered. They will get applied
896 | on the next commit.
897 |
898 | The compositor can use this information to allow or disallow
899 | different states like maximize or fullscreen and draw accurate
900 | animations.
901 |
902 | Similarly, a tiling window manager may use this information to
903 | place and resize client windows in a more effective way.
904 |
905 | The client should not rely on the compositor to obey the minimum
906 | size. The compositor may decide to ignore the values set by the
907 | client and request a smaller size.
908 |
909 | If never set, or a value of zero in the request, means that the
910 | client has no expected minimum size in the given dimension.
911 | As a result, a client wishing to reset the minimum size
912 | to an unspecified state can use zero for width and height in the
913 | request.
914 |
915 | Requesting a minimum size to be larger than the maximum size of
916 | a surface is illegal and will result in a protocol error.
917 |
918 | The width and height must be greater than or equal to zero. Using
919 | strictly negative values for width and height will result in a
920 | protocol error.
921 |
922 |
923 |
924 |
925 |
926 |
927 |
928 | Maximize the surface.
929 |
930 | After requesting that the surface should be maximized, the compositor
931 | will respond by emitting a configure event. Whether this configure
932 | actually sets the window maximized is subject to compositor policies.
933 | The client must then update its content, drawing in the configured
934 | state. The client must also acknowledge the configure when committing
935 | the new content (see ack_configure).
936 |
937 | It is up to the compositor to decide how and where to maximize the
938 | surface, for example which output and what region of the screen should
939 | be used.
940 |
941 | If the surface was already maximized, the compositor will still emit
942 | a configure event with the "maximized" state.
943 |
944 | If the surface is in a fullscreen state, this request has no direct
945 | effect. It may alter the state the surface is returned to when
946 | unmaximized unless overridden by the compositor.
947 |
948 |
949 |
950 |
951 |
952 | Unmaximize the surface.
953 |
954 | After requesting that the surface should be unmaximized, the compositor
955 | will respond by emitting a configure event. Whether this actually
956 | un-maximizes the window is subject to compositor policies.
957 | If available and applicable, the compositor will include the window
958 | geometry dimensions the window had prior to being maximized in the
959 | configure event. The client must then update its content, drawing it in
960 | the configured state. The client must also acknowledge the configure
961 | when committing the new content (see ack_configure).
962 |
963 | It is up to the compositor to position the surface after it was
964 | unmaximized; usually the position the surface had before maximizing, if
965 | applicable.
966 |
967 | If the surface was already not maximized, the compositor will still
968 | emit a configure event without the "maximized" state.
969 |
970 | If the surface is in a fullscreen state, this request has no direct
971 | effect. It may alter the state the surface is returned to when
972 | unmaximized unless overridden by the compositor.
973 |
974 |
975 |
976 |
977 |
978 | Make the surface fullscreen.
979 |
980 | After requesting that the surface should be fullscreened, the
981 | compositor will respond by emitting a configure event. Whether the
982 | client is actually put into a fullscreen state is subject to compositor
983 | policies. The client must also acknowledge the configure when
984 | committing the new content (see ack_configure).
985 |
986 | The output passed by the request indicates the client's preference as
987 | to which display it should be set fullscreen on. If this value is NULL,
988 | it's up to the compositor to choose which display will be used to map
989 | this surface.
990 |
991 | If the surface doesn't cover the whole output, the compositor will
992 | position the surface in the center of the output and compensate with
993 | with border fill covering the rest of the output. The content of the
994 | border fill is undefined, but should be assumed to be in some way that
995 | attempts to blend into the surrounding area (e.g. solid black).
996 |
997 | If the fullscreened surface is not opaque, the compositor must make
998 | sure that other screen content not part of the same surface tree (made
999 | up of subsurfaces, popups or similarly coupled surfaces) are not
1000 | visible below the fullscreened surface.
1001 |
1002 |
1003 |
1004 |
1005 |
1006 |
1007 | Make the surface no longer fullscreen.
1008 |
1009 | After requesting that the surface should be unfullscreened, the
1010 | compositor will respond by emitting a configure event.
1011 | Whether this actually removes the fullscreen state of the client is
1012 | subject to compositor policies.
1013 |
1014 | Making a surface unfullscreen sets states for the surface based on the following:
1015 | * the state(s) it may have had before becoming fullscreen
1016 | * any state(s) decided by the compositor
1017 | * any state(s) requested by the client while the surface was fullscreen
1018 |
1019 | The compositor may include the previous window geometry dimensions in
1020 | the configure event, if applicable.
1021 |
1022 | The client must also acknowledge the configure when committing the new
1023 | content (see ack_configure).
1024 |
1025 |
1026 |
1027 |
1028 |
1029 | Request that the compositor minimize your surface. There is no
1030 | way to know if the surface is currently minimized, nor is there
1031 | any way to unset minimization on this surface.
1032 |
1033 | If you are looking to throttle redrawing when minimized, please
1034 | instead use the wl_surface.frame event for this, as this will
1035 | also work with live previews on windows in Alt-Tab, Expose or
1036 | similar compositor features.
1037 |
1038 |
1039 |
1040 |
1041 |
1042 | This configure event asks the client to resize its toplevel surface or
1043 | to change its state. The configured state should not be applied
1044 | immediately. See xdg_surface.configure for details.
1045 |
1046 | The width and height arguments specify a hint to the window
1047 | about how its surface should be resized in window geometry
1048 | coordinates. See set_window_geometry.
1049 |
1050 | If the width or height arguments are zero, it means the client
1051 | should decide its own window dimension. This may happen when the
1052 | compositor needs to configure the state of the surface but doesn't
1053 | have any information about any previous or expected dimension.
1054 |
1055 | The states listed in the event specify how the width/height
1056 | arguments should be interpreted, and possibly how it should be
1057 | drawn.
1058 |
1059 | Clients must send an ack_configure in response to this event. See
1060 | xdg_surface.configure and xdg_surface.ack_configure for details.
1061 |
1062 |
1063 |
1064 |
1065 |
1066 |
1067 |
1068 |
1069 | The close event is sent by the compositor when the user
1070 | wants the surface to be closed. This should be equivalent to
1071 | the user clicking the close button in client-side decorations,
1072 | if your application has any.
1073 |
1074 | This is only a request that the user intends to close the
1075 | window. The client may choose to ignore this request, or show
1076 | a dialog to ask the user to save their data, etc.
1077 |
1078 |
1079 |
1080 |
1081 |
1082 |
1083 |
1084 | The configure_bounds event may be sent prior to a xdg_toplevel.configure
1085 | event to communicate the bounds a window geometry size is recommended
1086 | to constrain to.
1087 |
1088 | The passed width and height are in surface coordinate space. If width
1089 | and height are 0, it means bounds is unknown and equivalent to as if no
1090 | configure_bounds event was ever sent for this surface.
1091 |
1092 | The bounds can for example correspond to the size of a monitor excluding
1093 | any panels or other shell components, so that a surface isn't created in
1094 | a way that it cannot fit.
1095 |
1096 | The bounds may change at any point, and in such a case, a new
1097 | xdg_toplevel.configure_bounds will be sent, followed by
1098 | xdg_toplevel.configure and xdg_surface.configure.
1099 |
1100 |
1101 |
1102 |
1103 |
1104 |
1105 |
1106 |
1107 |
1108 |
1109 |
1110 |
1111 |
1112 |
1113 |
1114 |
1115 | This event advertises the capabilities supported by the compositor. If
1116 | a capability isn't supported, clients should hide or disable the UI
1117 | elements that expose this functionality. For instance, if the
1118 | compositor doesn't advertise support for minimized toplevels, a button
1119 | triggering the set_minimized request should not be displayed.
1120 |
1121 | The compositor will ignore requests it doesn't support. For instance,
1122 | a compositor which doesn't advertise support for minimized will ignore
1123 | set_minimized requests.
1124 |
1125 | Compositors must send this event once before the first
1126 | xdg_surface.configure event. When the capabilities change, compositors
1127 | must send this event again and then send an xdg_surface.configure
1128 | event.
1129 |
1130 | The configured state should not be applied immediately. See
1131 | xdg_surface.configure for details.
1132 |
1133 | The capabilities are sent as an array of 32-bit unsigned integers in
1134 | native endianness.
1135 |
1136 |
1137 |
1138 |
1139 |
1140 |
1141 |
1142 | A popup surface is a short-lived, temporary surface. It can be used to
1143 | implement for example menus, popovers, tooltips and other similar user
1144 | interface concepts.
1145 |
1146 | A popup can be made to take an explicit grab. See xdg_popup.grab for
1147 | details.
1148 |
1149 | When the popup is dismissed, a popup_done event will be sent out, and at
1150 | the same time the surface will be unmapped. See the xdg_popup.popup_done
1151 | event for details.
1152 |
1153 | Explicitly destroying the xdg_popup object will also dismiss the popup and
1154 | unmap the surface. Clients that want to dismiss the popup when another
1155 | surface of their own is clicked should dismiss the popup using the destroy
1156 | request.
1157 |
1158 | A newly created xdg_popup will be stacked on top of all previously created
1159 | xdg_popup surfaces associated with the same xdg_toplevel.
1160 |
1161 | The parent of an xdg_popup must be mapped (see the xdg_surface
1162 | description) before the xdg_popup itself.
1163 |
1164 | The client must call wl_surface.commit on the corresponding wl_surface
1165 | for the xdg_popup state to take effect.
1166 |
1167 |
1168 |
1169 |
1171 |
1172 |
1173 |
1174 |
1175 | This destroys the popup. Explicitly destroying the xdg_popup
1176 | object will also dismiss the popup, and unmap the surface.
1177 |
1178 | If this xdg_popup is not the "topmost" popup, a protocol error
1179 | will be sent.
1180 |
1181 |
1182 |
1183 |
1184 |
1185 | This request makes the created popup take an explicit grab. An explicit
1186 | grab will be dismissed when the user dismisses the popup, or when the
1187 | client destroys the xdg_popup. This can be done by the user clicking
1188 | outside the surface, using the keyboard, or even locking the screen
1189 | through closing the lid or a timeout.
1190 |
1191 | If the compositor denies the grab, the popup will be immediately
1192 | dismissed.
1193 |
1194 | This request must be used in response to some sort of user action like a
1195 | button press, key press, or touch down event. The serial number of the
1196 | event should be passed as 'serial'.
1197 |
1198 | The parent of a grabbing popup must either be an xdg_toplevel surface or
1199 | another xdg_popup with an explicit grab. If the parent is another
1200 | xdg_popup it means that the popups are nested, with this popup now being
1201 | the topmost popup.
1202 |
1203 | Nested popups must be destroyed in the reverse order they were created
1204 | in, e.g. the only popup you are allowed to destroy at all times is the
1205 | topmost one.
1206 |
1207 | When compositors choose to dismiss a popup, they may dismiss every
1208 | nested grabbing popup as well. When a compositor dismisses popups, it
1209 | will follow the same dismissing order as required from the client.
1210 |
1211 | If the topmost grabbing popup is destroyed, the grab will be returned to
1212 | the parent of the popup, if that parent previously had an explicit grab.
1213 |
1214 | If the parent is a grabbing popup which has already been dismissed, this
1215 | popup will be immediately dismissed. If the parent is a popup that did
1216 | not take an explicit grab, an error will be raised.
1217 |
1218 | During a popup grab, the client owning the grab will receive pointer
1219 | and touch events for all their surfaces as normal (similar to an
1220 | "owner-events" grab in X11 parlance), while the top most grabbing popup
1221 | will always have keyboard focus.
1222 |
1223 |
1225 |
1226 |
1227 |
1228 |
1229 |
1230 | This event asks the popup surface to configure itself given the
1231 | configuration. The configured state should not be applied immediately.
1232 | See xdg_surface.configure for details.
1233 |
1234 | The x and y arguments represent the position the popup was placed at
1235 | given the xdg_positioner rule, relative to the upper left corner of the
1236 | window geometry of the parent surface.
1237 |
1238 | For version 2 or older, the configure event for an xdg_popup is only
1239 | ever sent once for the initial configuration. Starting with version 3,
1240 | it may be sent again if the popup is setup with an xdg_positioner with
1241 | set_reactive requested, or in response to xdg_popup.reposition requests.
1242 |
1243 |
1245 |
1247 |
1248 |
1249 |
1250 |
1251 |
1252 |
1253 | The popup_done event is sent out when a popup is dismissed by the
1254 | compositor. The client should destroy the xdg_popup object at this
1255 | point.
1256 |
1257 |
1258 |
1259 |
1260 |
1261 |
1262 |
1263 | Reposition an already-mapped popup. The popup will be placed given the
1264 | details in the passed xdg_positioner object, and a
1265 | xdg_popup.repositioned followed by xdg_popup.configure and
1266 | xdg_surface.configure will be emitted in response. Any parameters set
1267 | by the previous positioner will be discarded.
1268 |
1269 | The passed token will be sent in the corresponding
1270 | xdg_popup.repositioned event. The new popup position will not take
1271 | effect until the corresponding configure event is acknowledged by the
1272 | client. See xdg_popup.repositioned for details. The token itself is
1273 | opaque, and has no other special meaning.
1274 |
1275 | If multiple reposition requests are sent, the compositor may skip all
1276 | but the last one.
1277 |
1278 | If the popup is repositioned in response to a configure event for its
1279 | parent, the client should send an xdg_positioner.set_parent_configure
1280 | and possibly an xdg_positioner.set_parent_size request to allow the
1281 | compositor to properly constrain the popup.
1282 |
1283 | If the popup is repositioned together with a parent that is being
1284 | resized, but not in response to a configure event, the client should
1285 | send an xdg_positioner.set_parent_size request.
1286 |
1287 |
1288 |
1289 |
1290 |
1291 |
1292 |
1293 | The repositioned event is sent as part of a popup configuration
1294 | sequence, together with xdg_popup.configure and lastly
1295 | xdg_surface.configure to notify the completion of a reposition request.
1296 |
1297 | The repositioned event is to notify about the completion of a
1298 | xdg_popup.reposition request. The token argument is the token passed
1299 | in the xdg_popup.reposition request.
1300 |
1301 | Immediately after this event is emitted, xdg_popup.configure and
1302 | xdg_surface.configure will be sent with the updated size and position,
1303 | as well as a new configure serial.
1304 |
1305 | The client should optionally update the content of the popup, but must
1306 | acknowledge the new popup configuration for the new position to take
1307 | effect. See xdg_surface.ack_configure for details.
1308 |
1309 |
1310 |
1311 |
1312 |
1313 |
1314 |
--------------------------------------------------------------------------------
/deps/wayland-protocols/unstable/xdg-decoration/xdg-decoration-unstable-v1.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Copyright © 2018 Simon Ser
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a
7 | copy of this software and associated documentation files (the "Software"),
8 | to deal in the Software without restriction, including without limitation
9 | the rights to use, copy, modify, merge, publish, distribute, sublicense,
10 | and/or sell copies of the Software, and to permit persons to whom the
11 | Software is furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice (including the next
14 | paragraph) shall be included in all copies or substantial portions of the
15 | Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
20 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
23 | DEALINGS IN THE SOFTWARE.
24 |
25 |
26 |
27 |
28 | This interface allows a compositor to announce support for server-side
29 | decorations.
30 |
31 | A window decoration is a set of window controls as deemed appropriate by
32 | the party managing them, such as user interface components used to move,
33 | resize and change a window's state.
34 |
35 | A client can use this protocol to request being decorated by a supporting
36 | compositor.
37 |
38 | If compositor and client do not negotiate the use of a server-side
39 | decoration using this protocol, clients continue to self-decorate as they
40 | see fit.
41 |
42 | Warning! The protocol described in this file is experimental and
43 | backward incompatible changes may be made. Backward compatible changes
44 | may be added together with the corresponding interface version bump.
45 | Backward incompatible changes are done by bumping the version number in
46 | the protocol and interface names and resetting the interface version.
47 | Once the protocol is to be declared stable, the 'z' prefix and the
48 | version number in the protocol and interface names are removed and the
49 | interface version number is reset.
50 |
51 |
52 |
53 |
54 | Destroy the decoration manager. This doesn't destroy objects created
55 | with the manager.
56 |
57 |
58 |
59 |
60 |
61 | Create a new decoration object associated with the given toplevel.
62 |
63 | Creating an xdg_toplevel_decoration from an xdg_toplevel which has a
64 | buffer attached or committed is a client error, and any attempts by a
65 | client to attach or manipulate a buffer prior to the first
66 | xdg_toplevel_decoration.configure event must also be treated as
67 | errors.
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | The decoration object allows the compositor to toggle server-side window
77 | decorations for a toplevel surface. The client can request to switch to
78 | another mode.
79 |
80 | The xdg_toplevel_decoration object must be destroyed before its
81 | xdg_toplevel.
82 |
83 |
84 |
85 |
87 |
89 |
91 |
92 |
93 |
94 |
95 | Switch back to a mode without any server-side decorations at the next
96 | commit.
97 |
98 |
99 |
100 |
101 |
102 | These values describe window decoration modes.
103 |
104 |
106 |
108 |
109 |
110 |
111 |
112 | Set the toplevel surface decoration mode. This informs the compositor
113 | that the client prefers the provided decoration mode.
114 |
115 | After requesting a decoration mode, the compositor will respond by
116 | emitting an xdg_surface.configure event. The client should then update
117 | its content, drawing it without decorations if the received mode is
118 | server-side decorations. The client must also acknowledge the configure
119 | when committing the new content (see xdg_surface.ack_configure).
120 |
121 | The compositor can decide not to use the client's mode and enforce a
122 | different mode instead.
123 |
124 | Clients whose decoration mode depend on the xdg_toplevel state may send
125 | a set_mode request in response to an xdg_surface.configure event and wait
126 | for the next xdg_surface.configure event to prevent unwanted state.
127 | Such clients are responsible for preventing configure loops and must
128 | make sure not to send multiple successive set_mode requests with the
129 | same decoration mode.
130 |
131 |
132 |
133 |
134 |
135 |
136 | Unset the toplevel surface decoration mode. This informs the compositor
137 | that the client doesn't prefer a particular decoration mode.
138 |
139 | This request has the same semantics as set_mode.
140 |
141 |
142 |
143 |
144 |
145 | The configure event asks the client to change its decoration mode. The
146 | configured state should not be applied immediately. Clients must send an
147 | ack_configure in response to this event. See xdg_surface.configure and
148 | xdg_surface.ack_configure for details.
149 |
150 | A configure event can be sent at any time. The specified mode must be
151 | obeyed by the client.
152 |
153 |
154 |
155 |
156 |
157 |
--------------------------------------------------------------------------------
/shaders/compile.sh:
--------------------------------------------------------------------------------
1 | glslangValidator -V generic.vert -o generic.vert.spv
2 | glslangValidator -V generic.frag -o generic.frag.spv
3 | echo 'Done'
4 |
--------------------------------------------------------------------------------
/shaders/generic.frag:
--------------------------------------------------------------------------------
1 | #version 450
2 | #extension GL_ARB_separate_shader_objects : enable
3 |
4 | layout(binding = 0) uniform sampler2DArray samplerArray;
5 |
6 | layout(location = 0) in vec2 inTexCoord;
7 | layout(location = 1) in vec4 inColor;
8 |
9 | layout(location = 0) out vec4 outColor;
10 |
11 | layout( push_constant ) uniform Block {
12 | vec2 dimensions;
13 | float frame;
14 | } PushConstant;
15 |
16 | void main() {
17 | outColor = texture(samplerArray, vec3(inTexCoord, 0)) * inColor;
18 | }
19 |
--------------------------------------------------------------------------------
/shaders/generic.frag.spv:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kdchambers/vkwayland/266af65c0a9ef97f6218878cfa78d9f176be9d04/shaders/generic.frag.spv
--------------------------------------------------------------------------------
/shaders/generic.vert:
--------------------------------------------------------------------------------
1 | #version 450
2 | #extension GL_ARB_separate_shader_objects : enable
3 |
4 | layout(location = 0) in vec2 inPosition;
5 | layout(location = 1) in vec2 inTexCoord;
6 | layout(location = 2) in vec4 inColor;
7 |
8 | layout(location = 0) out vec2 outTexCoord;
9 | layout(location = 1) out vec4 outColor;
10 |
11 | void main() {
12 | gl_Position = vec4(inPosition.x, inPosition.y, 0.0, 1.0);
13 | outColor = inColor;
14 | outTexCoord = inTexCoord;
15 | }
--------------------------------------------------------------------------------
/shaders/generic.vert.spv:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kdchambers/vkwayland/266af65c0a9ef97f6218878cfa78d9f176be9d04/shaders/generic.vert.spv
--------------------------------------------------------------------------------
/shaders/shaders.zig:
--------------------------------------------------------------------------------
1 | const fragment_shader_path = "generic.frag.spv";
2 | const vertex_shader_path = "generic.vert.spv";
3 |
4 | pub const fragment_spv align(4) = @embedFile(fragment_shader_path);
5 | pub const vertex_spv align(4) = @embedFile(vertex_shader_path);
--------------------------------------------------------------------------------
/src/main.zig:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | // Copyright (c) 2024 Keith Chambers
3 |
4 | const std = @import("std");
5 | const builtin = @import("builtin");
6 | const vk = @import("vulkan");
7 | const vulkan_config = @import("vulkan_config.zig");
8 | const img = @import("zigimg");
9 | const shaders = @import("shaders");
10 |
11 | const wayland = @import("wayland");
12 | const wl = wayland.client.wl;
13 | const xdg = wayland.client.xdg;
14 | const zxdg = wayland.client.zxdg;
15 |
16 | const clib = @cImport({
17 | @cInclude("dlfcn.h");
18 | });
19 |
20 | // TODO:
21 | // - Audit memory allocation overall
22 | // - Audit logging
23 | // - vulkan: Audit staging_buffer code
24 | // - vulkan: Use a separate memory type for texture data
25 |
26 | // ====== Contents ======
27 | //
28 | // 1. Options
29 | // 2. Globals
30 | // 3. Core Types + Functions
31 | // 4. Wayland Types + Functions
32 | // 5. Vulkan Code
33 | // 6. Vulkan Util / Wrapper Functions
34 | // 7. Print Functions
35 | // 8. Util + Misc
36 | //
37 | // ======================
38 |
39 | //
40 | // 1. Options
41 | //
42 |
43 | /// Change this to force the log level. Otherwise it is determined by release mode
44 | pub const log_level: std.log.Level = .info;
45 |
46 | /// Screen dimensions of the application, as reported by wayland
47 | /// Initial values are arbirary and will be updated once the wayland
48 | /// server reports a change
49 | var screen_dimensions = geometry.Dimensions2D(ScreenPixelBaseType){
50 | .width = 1040,
51 | .height = 640,
52 | };
53 |
54 | /// Determines the memory allocated for storing mesh data
55 | /// Represents the number of quads that will be able to be drawn
56 | /// This can be a colored quad, or a textured quad such as a charactor
57 | const max_texture_quads_per_render: u32 = 1024;
58 |
59 | /// Maximum number of screen framebuffers to use
60 | /// 2-3 would be recommented to avoid screen tearing
61 | const max_frames_in_flight: u32 = 2;
62 |
63 | /// Enable Vulkan validation layers
64 | const enable_validation_layers = if (@import("builtin").mode == .Debug) true else false;
65 |
66 | const vulkan_engine_version = vk.makeApiVersion(0, 0, 1, 0);
67 | const vulkan_engine_name = "No engine";
68 | const vulkan_application_version = vk.makeApiVersion(0, 0, 1, 0);
69 | const application_name = "vkwayland";
70 |
71 | /// Enables transparency on the selected surface
72 | const transparancy_enabled = true;
73 |
74 | /// Color to use for icon images
75 | const icon_color = graphics.RGB(f32).fromInt(11, 11, 11);
76 |
77 | /// The transparency of the selected surface
78 | /// Valid values between 0.0 (no transparency) and 1.0 (full)
79 | /// Ignored if `transparancy_enabled` is false
80 | const transparancy_level = 0.0;
81 |
82 | /// Initial background color of application surface before transparency is applied
83 | /// Each color refers to a corner of the quad.
84 | var background_color_a = [4]graphics.RGB(f32){
85 | graphics.RGB(f32).fromInt(66, 77, 26), // Top Left
86 | graphics.RGB(f32).fromInt(65, 88, 200), // Top Right
87 | graphics.RGB(f32).fromInt(111, 89, 156), // Bottom Right
88 | graphics.RGB(f32).fromInt(28, 52, 55), // Bottom Left
89 | };
90 |
91 | /// Background color to transition to
92 | var background_color_b = [4]graphics.RGB(f32){
93 | graphics.RGB(f32).fromInt(255, 74, 255),
94 | graphics.RGB(f32).fromInt(65, 74, 255),
95 | graphics.RGB(f32).fromInt(73, 74, 111),
96 | graphics.RGB(f32).fromInt(133, 74, 255),
97 | };
98 |
99 | const window_decorations = struct {
100 | const height_pixels = 40;
101 | const color = graphics.RGBA(f32).fromInt(u8, 200, 200, 200, 255);
102 | const exit_button = struct {
103 | const size_pixels = 24;
104 | const color_hovered = graphics.RGBA(f32).fromInt(u8, 180, 180, 180, 255);
105 | };
106 | };
107 |
108 | /// The time in milliseconds for the background color to change
109 | /// from color a to b, then back to a
110 | const background_color_loop_ms: u32 = 1000 * 10;
111 |
112 | /// Version of Vulkan to use
113 | /// https://www.khronos.org/registry/vulkan/
114 | const vulkan_api_version = vk.API_VERSION_1_3;
115 |
116 | /// How many times the main loop should check for updates per second
117 | /// NOTE: This does not refer to how many times the screen is drawn to. That is driven by the
118 | /// `frameListener` callback that the wayland compositor will trigger when it's ready for
119 | /// another image. In present mode FIFO this should correspond to the monitors display rate
120 | /// using v-sync.
121 | /// However, `input_fps` can be used to limit / reduce the display refresh rate
122 | const input_fps: u32 = 30;
123 |
124 | /// Options to print various vulkan objects that will be selected at
125 | /// runtime based on the hardware / system that is available
126 | const print_vulkan_objects = struct {
127 | /// Capabilities of all available memory types
128 | const memory_type_all: bool = true;
129 | /// Capabilities of the selected surface
130 | /// VSync, transparency, etc
131 | const surface_abilties: bool = true;
132 | };
133 |
134 | //
135 | // Bonus: Options you shouldn't change
136 | //
137 |
138 | const enable_blending = true;
139 |
140 | //
141 | // 2. Globals
142 | //
143 |
144 | const fragment_shader_path = "../shaders/generic.frag.spv";
145 | const vertex_shader_path = "../shaders/generic.vert.spv";
146 |
147 | // NOTE: The following points aren't used in the code, but are useful to know
148 | // http://anki3d.org/vulkan-coordinate-system/
149 | const ScreenPoint = geometry.Coordinates2D(ScreenNormalizedBaseType);
150 | const point_top_left = ScreenPoint{ .x = -1.0, .y = -1.0 };
151 | const point_top_right = ScreenPoint{ .x = 1.0, .y = -1.0 };
152 | const point_bottom_left = ScreenPoint{ .x = -1.0, .y = 1.0 };
153 | const point_bottom_right = ScreenPoint{ .x = 1.0, .y = 1.0 };
154 |
155 | /// Defines the entire surface area of a screen in vulkans coordinate system
156 | /// I.e normalized device coordinates right (ndc right)
157 | const full_screen_extent = geometry.Extent2D(ScreenNormalizedBaseType){
158 | .x = -1.0,
159 | .y = -1.0,
160 | .width = 2.0,
161 | .height = 2.0,
162 | };
163 |
164 | /// Defines the entire surface area of a texture
165 | const full_texture_extent = geometry.Extent2D(TextureNormalizedBaseType){
166 | .x = 0.0,
167 | .y = 0.0,
168 | .width = 1.0,
169 | .height = 1.0,
170 | };
171 |
172 | const asset_path_icon = "assets/icons/";
173 |
174 | const IconType = enum {
175 | add,
176 | arrow_back,
177 | check_circle,
178 | close,
179 | delete,
180 | favorite,
181 | home,
182 | logout,
183 | menu,
184 | search,
185 | settings,
186 | star,
187 | };
188 |
189 | const icon_texture_row_count: u32 = 4;
190 | const icon_texture_column_count: u32 = 3;
191 |
192 | const icon_path_list = [_][]const u8{
193 | asset_path_icon ++ "add.png",
194 | asset_path_icon ++ "arrow_back.png",
195 | asset_path_icon ++ "check_circle.png",
196 | asset_path_icon ++ "close.png",
197 | asset_path_icon ++ "delete.png",
198 | asset_path_icon ++ "favorite.png",
199 | asset_path_icon ++ "home.png",
200 | asset_path_icon ++ "logout.png",
201 | asset_path_icon ++ "menu.png",
202 | asset_path_icon ++ "search.png",
203 | asset_path_icon ++ "settings.png",
204 | asset_path_icon ++ "star.png",
205 | };
206 |
207 | /// Returns the normalized coordinates of the icon in the texture image
208 | fn iconTextureLookup(icon_type: IconType) geometry.Coordinates2D(f32) {
209 | const icon_type_index = @intFromEnum(icon_type);
210 | const x: u32 = icon_type_index % icon_texture_row_count;
211 | const y: u32 = icon_type_index / icon_texture_row_count;
212 | const x_pixel = x * icon_dimensions.width;
213 | const y_pixel = y * icon_dimensions.height;
214 | return .{
215 | .x = @as(f32, @floatFromInt(x_pixel)) / @as(f32, @floatFromInt(texture_layer_dimensions.width)),
216 | .y = @as(f32, @floatFromInt(y_pixel)) / @as(f32, @floatFromInt(texture_layer_dimensions.height)),
217 | };
218 | }
219 |
220 | /// Icon dimensions in pixels
221 | const icon_dimensions = geometry.Dimensions2D(u32){
222 | .width = 48,
223 | .height = 48,
224 | };
225 |
226 | const icon_texture_dimensions = geometry.Dimensions2D(u32){
227 | .width = icon_dimensions.width * icon_texture_row_count,
228 | .height = icon_dimensions.height * icon_texture_column_count,
229 | };
230 |
231 | // NOTE: The max texture size that is guaranteed is 4096 * 4096
232 | // Support for larger textures will need to be queried
233 | // https://github.com/gpuweb/gpuweb/issues/1327
234 | const texture_layer_dimensions = geometry.Dimensions2D(TexturePixelBaseType){
235 | .width = 512,
236 | .height = 512,
237 | };
238 |
239 | /// Size in bytes of each texture layer (Not including padding, etc)
240 | const texture_layer_size = @sizeOf(graphics.RGBA(f32)) * @as(u64, @intCast(texture_layer_dimensions.width)) * texture_layer_dimensions.height;
241 |
242 | const indices_range_index_begin = 0;
243 | const indices_range_size = max_texture_quads_per_render * @sizeOf(u16) * 6; // 12 kb
244 | const indices_range_count = indices_range_size / @sizeOf(u16);
245 | const vertices_range_index_begin = indices_range_size;
246 | const vertices_range_size = max_texture_quads_per_render * @sizeOf(graphics.GenericVertex) * 4; // 80 kb
247 | const vertices_range_count = vertices_range_size / @sizeOf(graphics.GenericVertex);
248 | const memory_size = indices_range_size + vertices_range_size;
249 |
250 | /// Vertices to be reserved at the beginning of our buffer and
251 | /// not overriden in our draw function
252 | /// Here we're just reserving the background quad
253 | const vertices_reserved_range_count = 1;
254 |
255 | var quad_face_writer_pool: QuadFaceWriterPool(graphics.GenericVertex) = undefined;
256 |
257 | const validation_layers = if (enable_validation_layers)
258 | [1][*:0]const u8{"VK_LAYER_KHRONOS_validation"}
259 | else
260 | [*:0]const u8{};
261 |
262 | const device_extensions = [_][*:0]const u8{vk.extension_info.khr_swapchain.name};
263 | const surface_extensions = [_][*:0]const u8{ "VK_KHR_surface", "VK_KHR_wayland_surface" };
264 |
265 | var is_draw_required: bool = true;
266 | var is_render_requested: bool = true;
267 | var is_shutdown_requested: bool = false;
268 |
269 | /// Set when command buffers need to be (re)recorded. The following will cause that to happen
270 | /// 1. First command buffer recording
271 | /// 2. Screen resized
272 | /// 3. Push constants need to be updated
273 | /// 4. Number of vertices to be drawn has changed
274 | var is_record_requested: bool = true;
275 |
276 | var vertex_buffer: []graphics.QuadFace(graphics.GenericVertex) = undefined;
277 |
278 | var current_frame: u32 = 0;
279 | var previous_frame: u32 = 0;
280 |
281 | var framebuffer_resized: bool = true;
282 | var mapped_device_memory: [*]u8 = undefined;
283 |
284 | var wayland_client: WaylandClient = undefined;
285 | var alpha_mode: vk.CompositeAlphaFlagsKHR = .{ .opaque_bit_khr = true };
286 |
287 | var mouse_coordinates = geometry.Coordinates2D(f64){ .x = 0.0, .y = 0.0 };
288 | var is_mouse_in_screen = false;
289 |
290 | var vertex_buffer_quad_count: u32 = 0;
291 |
292 | var texture_image_view: vk.ImageView = undefined;
293 | var texture_image: vk.Image = undefined;
294 | var texture_vertices_buffer: vk.Buffer = undefined;
295 | var texture_indices_buffer: vk.Buffer = undefined;
296 |
297 | var texture_memory_map: [*]graphics.RGBA(f32) = undefined;
298 |
299 | var background_color_loop_time_base: i64 = undefined;
300 | var draw_window_decorations_requested: bool = true;
301 |
302 | /// Pointer to quad that will be reused to control the background color of the application
303 | /// An alternative method, would be to use the clear_colors parameter when recording a render pass
304 | /// However, this allows us to avoid re-recording commands buffers, etc
305 | var background_quad: *graphics.QuadFace(graphics.GenericVertex) = undefined;
306 |
307 | const texture_size_bytes = texture_layer_dimensions.width * texture_layer_dimensions.height * @sizeOf(graphics.RGBA(f32));
308 |
309 | // Used to collecting some basic performance data
310 | var frame_count: u64 = 0;
311 | var slowest_frame_ns: u64 = 0;
312 | var fastest_frame_ns: u64 = std.math.maxInt(u64);
313 | var frame_duration_total_ns: u64 = 0;
314 | var frame_duration_awake_ns: u64 = 0;
315 |
316 | /// When clicked, terminate the application
317 | var exit_button_extent: geometry.Extent2D(u16) = undefined;
318 | var exit_button_background_quad: *graphics.QuadFace(graphics.GenericVertex) = undefined;
319 | var exit_button_hovered: bool = false;
320 |
321 | var vkGetInstanceProcAddr: *const fn (instance: vk.Instance, procname: [*:0]const u8) vk.PfnVoidFunction = undefined;
322 |
323 | //
324 | // 3. Core Types + Functions
325 | //
326 |
327 | pub const VKProc = fn () callconv(.C) void;
328 |
329 | extern fn vkGetPhysicalDevicePresentationSupport(instance: vk.Instance, pdev: vk.PhysicalDevice, queuefamily: u32) c_int;
330 |
331 | const ScreenPixelBaseType = u16;
332 | const ScreenNormalizedBaseType = f32;
333 |
334 | const TexturePixelBaseType = u16;
335 | const TextureNormalizedBaseType = f32;
336 |
337 | const GraphicsContext = struct {
338 | base_dispatch: vulkan_config.BaseDispatch,
339 | instance_dispatch: vulkan_config.InstanceDispatch,
340 | device_dispatch: vulkan_config.DeviceDispatch,
341 |
342 | vertex_shader_module: vk.ShaderModule,
343 | fragment_shader_module: vk.ShaderModule,
344 |
345 | render_pass: vk.RenderPass,
346 | framebuffers: []vk.Framebuffer,
347 | graphics_pipeline: vk.Pipeline,
348 | descriptor_pool: vk.DescriptorPool,
349 | descriptor_sets: []vk.DescriptorSet,
350 | descriptor_set_layouts: []vk.DescriptorSetLayout,
351 | pipeline_layout: vk.PipelineLayout,
352 | sampler: vk.Sampler,
353 | image_memory: vk.DeviceMemory,
354 | mesh_memory: vk.DeviceMemory,
355 |
356 | instance: vk.Instance,
357 | surface: vk.SurfaceKHR,
358 | surface_format: vk.SurfaceFormatKHR,
359 | physical_device: vk.PhysicalDevice,
360 | logical_device: vk.Device,
361 | graphics_present_queue: vk.Queue, // Same queue used for graphics + presenting
362 | graphics_present_queue_index: u32,
363 | swapchain_min_image_count: u32,
364 | swapchain: vk.SwapchainKHR,
365 | swapchain_extent: vk.Extent2D,
366 | swapchain_images: []vk.Image,
367 | swapchain_image_views: []vk.ImageView,
368 | command_pool: vk.CommandPool,
369 | command_buffers: []vk.CommandBuffer,
370 | images_available: []vk.Semaphore,
371 | renders_finished: []vk.Semaphore,
372 | inflight_fences: []vk.Fence,
373 | };
374 |
375 | /// Used to allocate QuadFaceWriters that share backing memory
376 | fn QuadFaceWriterPool(comptime VertexType: type) type {
377 | return struct {
378 | const QuadFace = graphics.QuadFace;
379 |
380 | memory_ptr: [*]QuadFace(VertexType),
381 | memory_quad_range: u32,
382 |
383 | pub fn initialize(start: [*]align(@alignOf(VertexType)) u8, memory_quad_range: u32) @This() {
384 | return .{
385 | .memory_ptr = @as([*]QuadFace(VertexType), @ptrCast(start)),
386 | .memory_quad_range = memory_quad_range,
387 | };
388 | }
389 |
390 | pub fn create(self: *@This(), quad_index: u16, quad_size: u16) QuadFaceWriter(VertexType) {
391 | std.debug.assert((quad_index + quad_size) <= self.memory_quad_range);
392 | return QuadFaceWriter(VertexType).initialize(self.memory_ptr, quad_index, quad_size);
393 | }
394 | };
395 | }
396 |
397 | fn QuadFaceWriter(comptime VertexType: type) type {
398 | return struct {
399 | const QuadFace = graphics.QuadFace;
400 |
401 | memory_ptr: [*]QuadFace(VertexType),
402 |
403 | quad_index: u32,
404 | capacity: u32,
405 | used: u32 = 0,
406 |
407 | pub fn initialize(base: [*]QuadFace(VertexType), quad_index: u32, quad_size: u32) @This() {
408 | return .{
409 | .memory_ptr = @as([*]QuadFace(VertexType), @ptrCast(&base[quad_index])),
410 | .quad_index = quad_index,
411 | .capacity = quad_size,
412 | .used = 0,
413 | };
414 | }
415 |
416 | pub fn indexFromBase(self: @This()) u32 {
417 | return self.quad_index + self.used;
418 | }
419 |
420 | pub fn remaining(self: *@This()) u32 {
421 | std.debug.assert(self.capacity >= self.used);
422 | return @as(u32, @intCast(self.capacity - self.used));
423 | }
424 |
425 | pub fn reset(self: *@This()) void {
426 | self.used = 0;
427 | }
428 |
429 | pub fn create(self: *@This()) !*QuadFace(VertexType) {
430 | if (self.used == self.capacity) return error.OutOfMemory;
431 | defer self.used += 1;
432 | return &self.memory_ptr[self.used];
433 | }
434 |
435 | pub fn allocate(self: *@This(), amount: u32) ![]QuadFace(VertexType) {
436 | if ((self.used + amount) > self.capacity) return error.OutOfMemory;
437 | defer self.used += amount;
438 | return self.memory_ptr[self.used .. self.used + amount];
439 | }
440 | };
441 | }
442 |
443 | const graphics = struct {
444 | fn TypeOfField(comptime t: anytype, comptime field_name: []const u8) type {
445 | for (@typeInfo(t).Struct.fields) |field| {
446 | if (std.mem.eql(u8, field.name, field_name)) {
447 | return field.type;
448 | }
449 | }
450 | unreachable;
451 | }
452 |
453 | pub const AnchorPoint = enum {
454 | center,
455 | top_left,
456 | top_right,
457 | bottom_left,
458 | bottom_right,
459 | };
460 |
461 | pub fn generateQuad(
462 | comptime VertexType: type,
463 | extent: geometry.Extent2D(TypeOfField(VertexType, "x")),
464 | comptime anchor_point: AnchorPoint,
465 | ) QuadFace(VertexType) {
466 | std.debug.assert(TypeOfField(VertexType, "x") == TypeOfField(VertexType, "y"));
467 | return switch (anchor_point) {
468 | .top_left => [_]VertexType{
469 | // zig fmt: off
470 | .{ .x = extent.x, .y = extent.y }, // Top Left
471 | .{ .x = extent.x + extent.width, .y = extent.y }, // Top Right
472 | .{ .x = extent.x + extent.width, .y = extent.y + extent.height }, // Bottom Right
473 | .{ .x = extent.x, .y = extent.y + extent.height }, // Bottom Left
474 | },
475 | .bottom_left => [_]VertexType{
476 | .{ .x = extent.x, .y = extent.y - extent.height }, // Top Left
477 | .{ .x = extent.x + extent.width, .y = extent.y - extent.height }, // Top Right
478 | .{ .x = extent.x + extent.width, .y = extent.y }, // Bottom Right
479 | .{ .x = extent.x, .y = extent.y }, // Bottom Left
480 | },
481 | .center => [_]VertexType{
482 | .{ .x = extent.x - (extent.width / 2.0), .y = extent.y - (extent.height / 2.0) }, // Top Left
483 | .{ .x = extent.x + (extent.width / 2.0), .y = extent.y - (extent.height / 2.0) }, // Top Right
484 | .{ .x = extent.x + (extent.width / 2.0), .y = extent.y + (extent.height / 2.0) }, // Bottom Right
485 | .{ .x = extent.x - (extent.width / 2.0), .y = extent.y + (extent.height / 2.0) }, // Bottom Left
486 | // zig fmt: on
487 | },
488 | else => @compileError("Invalid AnchorPoint"),
489 | };
490 | }
491 |
492 | pub fn generateTexturedQuad(
493 | comptime VertexType: type,
494 | extent: geometry.Extent2D(TypeOfField(VertexType, "x")),
495 | texture_extent: geometry.Extent2D(TypeOfField(VertexType, "tx")),
496 | comptime anchor_point: AnchorPoint,
497 | ) QuadFace(VertexType) {
498 | std.debug.assert(TypeOfField(VertexType, "x") == TypeOfField(VertexType, "y"));
499 | std.debug.assert(TypeOfField(VertexType, "tx") == TypeOfField(VertexType, "ty"));
500 | var base_quad = generateQuad(VertexType, extent, anchor_point);
501 | base_quad[0].tx = texture_extent.x;
502 | base_quad[0].ty = texture_extent.y;
503 | base_quad[1].tx = texture_extent.x + texture_extent.width;
504 | base_quad[1].ty = texture_extent.y;
505 | base_quad[2].tx = texture_extent.x + texture_extent.width;
506 | base_quad[2].ty = texture_extent.y + texture_extent.height;
507 | base_quad[3].tx = texture_extent.x;
508 | base_quad[3].ty = texture_extent.y + texture_extent.height;
509 | return base_quad;
510 | }
511 |
512 | pub fn generateQuadColored(
513 | comptime VertexType: type,
514 | extent: geometry.Extent2D(TypeOfField(VertexType, "x")),
515 | quad_color: RGBA(f32),
516 | comptime anchor_point: AnchorPoint,
517 | ) QuadFace(VertexType) {
518 | std.debug.assert(TypeOfField(VertexType, "x") == TypeOfField(VertexType, "y"));
519 | var base_quad = generateQuad(VertexType, extent, anchor_point);
520 | base_quad[0].color = quad_color;
521 | base_quad[1].color = quad_color;
522 | base_quad[2].color = quad_color;
523 | base_quad[3].color = quad_color;
524 | return base_quad;
525 | }
526 |
527 | pub const GenericVertex = packed struct {
528 | x: f32 = 1.0,
529 | y: f32 = 1.0,
530 | // This default value references the last pixel in our texture which
531 | // we set all values to 1.0 so that we can use it to multiply a color
532 | // without changing it. See fragment shader
533 | tx: f32 = 1.0,
534 | ty: f32 = 1.0,
535 | color: RGBA(f32) = .{
536 | .r = 1.0,
537 | .g = 1.0,
538 | .b = 1.0,
539 | .a = 1.0,
540 | },
541 |
542 | pub fn nullFace() QuadFace(GenericVertex) {
543 | return .{ .{}, .{}, .{}, .{} };
544 | }
545 | };
546 |
547 | pub fn QuadFace(comptime VertexType: type) type {
548 | return [4]VertexType;
549 | }
550 |
551 | pub fn RGB(comptime BaseType: type) type {
552 | return packed struct {
553 | pub fn fromInt(r: u8, g: u8, b: u8) @This() {
554 | return .{
555 | .r = @as(BaseType, @floatFromInt(r)) / 255.0,
556 | .g = @as(BaseType, @floatFromInt(g)) / 255.0,
557 | .b = @as(BaseType, @floatFromInt(b)) / 255.0,
558 | };
559 | }
560 |
561 | pub inline fn toRGBA(self: @This()) RGBA(BaseType) {
562 | return .{
563 | .r = self.r,
564 | .g = self.g,
565 | .b = self.b,
566 | .a = 1.0,
567 | };
568 | }
569 |
570 | r: BaseType,
571 | g: BaseType,
572 | b: BaseType,
573 | };
574 | }
575 |
576 | pub fn RGBA(comptime BaseType: type) type {
577 | return packed struct {
578 | pub fn fromInt(comptime IntType: type, r: IntType, g: IntType, b: IntType, a: IntType) @This() {
579 | return .{
580 | .r = @as(BaseType, @floatFromInt(r)) / 255.0,
581 | .g = @as(BaseType, @floatFromInt(g)) / 255.0,
582 | .b = @as(BaseType, @floatFromInt(b)) / 255.0,
583 | .a = @as(BaseType, @floatFromInt(a)) / 255.0,
584 | };
585 | }
586 |
587 | pub inline fn isEqual(self: @This(), color: @This()) bool {
588 | return (self.r == color.r and self.g == color.g and self.b == color.b and self.a == color.a);
589 | }
590 |
591 | r: BaseType,
592 | g: BaseType,
593 | b: BaseType,
594 | a: BaseType,
595 | };
596 | }
597 | };
598 |
599 | const geometry = struct {
600 | pub fn Coordinates2D(comptime BaseType: type) type {
601 | return packed struct {
602 | x: BaseType,
603 | y: BaseType,
604 | };
605 | }
606 |
607 | pub fn Dimensions2D(comptime BaseType: type) type {
608 | return packed struct {
609 | height: BaseType,
610 | width: BaseType,
611 | };
612 | }
613 |
614 | pub fn Extent2D(comptime BaseType: type) type {
615 | return packed struct {
616 | x: BaseType,
617 | y: BaseType,
618 | height: BaseType,
619 | width: BaseType,
620 |
621 | inline fn isWithinBounds(self: @This(), comptime T: type, point: T) bool {
622 | const end_x = self.x + self.width;
623 | const end_y = self.y + self.height;
624 | return (point.x >= self.x and point.y >= self.y and point.x <= end_x and point.y <= end_y);
625 | }
626 | };
627 | }
628 | };
629 |
630 | /// Push constant structure that is used in our fragment shader
631 | const PushConstant = packed struct {
632 | width: f32,
633 | height: f32,
634 | frame: f32,
635 | };
636 |
637 | pub fn main() !void {
638 | var gpa = std.heap.GeneralPurposeAllocator(.{}){};
639 | defer _ = gpa.deinit();
640 |
641 | const allocator = gpa.allocator();
642 |
643 | var graphics_context: GraphicsContext = undefined;
644 |
645 | try setup(allocator, &graphics_context);
646 | try appLoop(allocator, &graphics_context);
647 |
648 | wayland_client.xdg_toplevel.destroy();
649 | wayland_client.xdg_surface.destroy();
650 | wayland_client.surface.destroy();
651 |
652 | cleanup(allocator, &graphics_context);
653 |
654 | waylandDeinit();
655 |
656 | std.log.info("Terminated cleanly", .{});
657 | }
658 |
659 | fn cleanup(allocator: std.mem.Allocator, app: *GraphicsContext) void {
660 |
661 | for (app.framebuffers) |framebuffer| {
662 | app.device_dispatch.destroyFramebuffer(app.logical_device, framebuffer, null);
663 | }
664 | allocator.free(app.framebuffers);
665 |
666 | app.device_dispatch.destroyPipeline(app.logical_device, app.graphics_pipeline, null);
667 | app.device_dispatch.destroySampler(app.logical_device, app.sampler, null);
668 |
669 | app.device_dispatch.destroyDescriptorPool(app.logical_device, app.descriptor_pool, null);
670 | app.device_dispatch.destroyPipelineLayout(app.logical_device, app.pipeline_layout, null);
671 | app.device_dispatch.destroyDescriptorSetLayout(app.logical_device, app.descriptor_set_layouts[0], null);
672 | allocator.free(app.descriptor_set_layouts);
673 | allocator.free(app.descriptor_sets);
674 |
675 | app.device_dispatch.destroyRenderPass(app.logical_device, app.render_pass, null);
676 |
677 | app.device_dispatch.freeCommandBuffers(app.logical_device, app.command_pool, @intCast(app.command_buffers.len), app.command_buffers.ptr);
678 | allocator.free(app.command_buffers);
679 |
680 | app.device_dispatch.destroyShaderModule(app.logical_device, app.vertex_shader_module, null);
681 | app.device_dispatch.destroyShaderModule(app.logical_device, app.fragment_shader_module, null);
682 |
683 | for (0..max_frames_in_flight) |i| {
684 | app.device_dispatch.destroySemaphore(app.logical_device, app.images_available[i], null);
685 | app.device_dispatch.destroySemaphore(app.logical_device, app.renders_finished[i], null);
686 | app.device_dispatch.destroyFence(app.logical_device, app.inflight_fences[i], null);
687 | }
688 |
689 | allocator.free(app.images_available);
690 | allocator.free(app.renders_finished);
691 | allocator.free(app.inflight_fences);
692 |
693 | app.device_dispatch.destroyCommandPool(app.logical_device, app.command_pool, null);
694 |
695 | app.device_dispatch.unmapMemory(app.logical_device, app.image_memory);
696 | app.device_dispatch.destroyImage(app.logical_device, texture_image, null);
697 | app.device_dispatch.destroyImageView(app.logical_device, texture_image_view, null);
698 | app.device_dispatch.freeMemory(app.logical_device, app.image_memory, null);
699 |
700 | app.device_dispatch.unmapMemory(app.logical_device, app.mesh_memory);
701 | mapped_device_memory = undefined;
702 |
703 | app.device_dispatch.destroyBuffer(app.logical_device, texture_vertices_buffer, null);
704 | app.device_dispatch.destroyBuffer(app.logical_device, texture_indices_buffer, null);
705 | app.device_dispatch.freeMemory(app.logical_device, app.mesh_memory, null);
706 |
707 | for (app.swapchain_image_views) |image_view| {
708 | app.device_dispatch.destroyImageView(app.logical_device, image_view, null);
709 | }
710 | app.device_dispatch.destroySwapchainKHR(app.logical_device, app.swapchain, null);
711 |
712 | allocator.free(app.swapchain_image_views);
713 | allocator.free(app.swapchain_images);
714 |
715 | app.device_dispatch.destroyDevice(app.logical_device, null);
716 |
717 | app.instance_dispatch.destroySurfaceKHR(app.instance, app.surface, null);
718 | app.instance_dispatch.destroyInstance(app.instance, null);
719 | }
720 |
721 | fn appLoop(allocator: std.mem.Allocator, app: *GraphicsContext) !void {
722 |
723 | const target_ms_per_frame: u32 = 1000 / input_fps;
724 | const target_ns_per_frame = target_ms_per_frame * std.time.ns_per_ms;
725 |
726 | std.log.info("Target milliseconds / frame: {d}", .{target_ms_per_frame});
727 |
728 | background_color_loop_time_base = std.time.milliTimestamp();
729 |
730 | while (!is_shutdown_requested) {
731 | frame_count += 1;
732 |
733 | const frame_start_ns = std.time.nanoTimestamp();
734 |
735 | // NOTE: Running this at a high `input_fps` (E.g 60) seems to put a lot of strain on
736 | // the wayland compositor. On my system with sway and river I've seen the
737 | // CPU usage of the compositor run 3 times that of this application in response
738 | // to this call alone.
739 | // TODO: Find a more efficient way to interact with the compositor if possible
740 | if (.SUCCESS != wayland_client.display.roundtrip()) {
741 | std.log.warn("wayland: Failed to do roundtrip to server", .{});
742 | }
743 |
744 | if (framebuffer_resized) {
745 | app.swapchain_extent.width = screen_dimensions.width;
746 | app.swapchain_extent.height = screen_dimensions.height;
747 | is_draw_required = true;
748 | framebuffer_resized = false;
749 | try recreateSwapchain(allocator, app);
750 | }
751 |
752 | if(is_draw_required) {
753 | is_draw_required = false;
754 | try draw();
755 | is_render_requested = true;
756 | is_record_requested = true;
757 | }
758 |
759 | if(is_render_requested) {
760 | is_render_requested = false;
761 |
762 | //
763 | // Update our background
764 | //
765 |
766 | const current_time = std.time.milliTimestamp();
767 | std.debug.assert(current_time >= background_color_loop_time_base);
768 |
769 | const loop_ms: f64 = @floatFromInt(background_color_loop_ms);
770 | var color_transition: f32 = @floatCast(@rem(@as(f64, @floatFromInt(current_time - background_color_loop_time_base)), loop_ms) / loop_ms);
771 | if(color_transition > 0.5) {
772 | color_transition = 1.0 - color_transition;
773 | }
774 |
775 | std.debug.assert(color_transition >= 0.0);
776 | std.debug.assert(color_transition <= 1.0);
777 |
778 | background_quad[0].color = graphics.RGBA(f32){
779 | .r = lerp(background_color_a[0].r, background_color_b[0].r, color_transition),
780 | .g = lerp(background_color_a[0].g, background_color_b[0].g, color_transition),
781 | .b = lerp(background_color_a[0].b, background_color_b[0].b, color_transition),
782 | .a = 1.0
783 | };
784 | background_quad[1].color = graphics.RGBA(f32){
785 | .r = lerp(background_color_a[1].r, background_color_b[1].r, color_transition),
786 | .g = lerp(background_color_a[1].g, background_color_b[1].g, color_transition),
787 | .b = lerp(background_color_a[1].b, background_color_b[1].b, color_transition),
788 | .a = 1.0
789 | };
790 | background_quad[2].color = graphics.RGBA(f32){
791 | .r = lerp(background_color_a[2].r, background_color_b[2].r, color_transition),
792 | .g = lerp(background_color_a[2].g, background_color_b[2].g, color_transition),
793 | .b = lerp(background_color_a[2].b, background_color_b[2].b, color_transition),
794 | .a = 1.0
795 | };
796 | background_quad[3].color = graphics.RGBA(f32){
797 | .r = lerp(background_color_a[3].r, background_color_b[3].r, color_transition),
798 | .g = lerp(background_color_a[3].g, background_color_b[3].g, color_transition),
799 | .b = lerp(background_color_a[3].b, background_color_b[3].b, color_transition),
800 | .a = 1.0
801 | };
802 |
803 | // Even though we're running at a constant loop, we don't always need to re-record command buffers
804 | if(is_record_requested) {
805 | is_record_requested = false;
806 | try recordRenderPass(app.*, vertex_buffer_quad_count * 6);
807 | }
808 |
809 | try renderFrame(allocator, app);
810 | }
811 |
812 | const frame_end_ns = std.time.nanoTimestamp();
813 | std.debug.assert(frame_end_ns >= frame_start_ns);
814 |
815 | const frame_duration_ns: u64 = @intCast(frame_end_ns - frame_start_ns);
816 |
817 | if (frame_duration_ns > slowest_frame_ns) {
818 | slowest_frame_ns = frame_duration_ns;
819 | }
820 |
821 | if (frame_duration_ns < fastest_frame_ns) {
822 | fastest_frame_ns = frame_duration_ns;
823 | }
824 |
825 | std.debug.assert(target_ns_per_frame > frame_duration_ns);
826 | const remaining_ns: u64 = target_ns_per_frame - @as(u64, @intCast(frame_duration_ns));
827 | std.debug.assert(remaining_ns <= target_ns_per_frame);
828 |
829 | const frame_work_completed_ns = std.time.nanoTimestamp();
830 | frame_duration_awake_ns += @as(u64, @intCast(frame_work_completed_ns - frame_start_ns));
831 |
832 | std.time.sleep(remaining_ns);
833 |
834 | const frame_completion_ns = std.time.nanoTimestamp();
835 | frame_duration_total_ns += @as(u64, @intCast(frame_completion_ns - frame_start_ns));
836 | }
837 |
838 | std.log.info("Run time: {d}", .{std.fmt.fmtDuration(frame_duration_total_ns)});
839 | std.log.info("Frame count: {d}", .{frame_count});
840 | std.log.info("Slowest: {}", .{std.fmt.fmtDuration(slowest_frame_ns)});
841 | std.log.info("Fastest: {}", .{std.fmt.fmtDuration(fastest_frame_ns)});
842 | std.log.info("Average: {}", .{std.fmt.fmtDuration((frame_duration_awake_ns / frame_count))});
843 |
844 | // For some reason the vkwayland is causing gnome shell to crash
845 | // on shutdown without this wait
846 | std.time.sleep(std.time.ns_per_ms * 50);
847 |
848 | try app.device_dispatch.deviceWaitIdle(app.logical_device);
849 | }
850 |
851 | /// Our example draw function
852 | /// This will run anytime the screen is resized
853 | fn draw() !void {
854 | const outer_margin_pixels: u32 = 20;
855 | const inner_margin_pixels: u32 = 10;
856 |
857 | const dimensions_pixels = geometry.Dimensions2D(u32) {
858 | .width = icon_dimensions.width,
859 | .height = icon_dimensions.height,
860 | };
861 |
862 | if(draw_window_decorations_requested and screen_dimensions.height <= window_decorations.height_pixels) {
863 | vertex_buffer_quad_count = 0;
864 | return;
865 | }
866 |
867 | const available_screen_dimensions = if(draw_window_decorations_requested)
868 | geometry.Dimensions2D(u16){ .height = screen_dimensions.height - window_decorations.height_pixels, .width = screen_dimensions.width}
869 | else screen_dimensions;
870 |
871 | const insufficient_horizontal_space = (available_screen_dimensions.width < (dimensions_pixels.width + outer_margin_pixels * 2));
872 | const insufficient_vertical_space = (available_screen_dimensions.height < (dimensions_pixels.height + outer_margin_pixels * 2));
873 |
874 | if(insufficient_horizontal_space or insufficient_vertical_space) {
875 | vertex_buffer_quad_count = 0;
876 | return;
877 | }
878 |
879 | var horizonal_quad_space_pixels = (available_screen_dimensions.width - (outer_margin_pixels * 2)) - dimensions_pixels.width;
880 | var horizonal_count: u32 = 1;
881 |
882 | {
883 | const required_space = dimensions_pixels.width + inner_margin_pixels;
884 | while(horizonal_quad_space_pixels >= required_space) {
885 | horizonal_count += 1;
886 | horizonal_quad_space_pixels -= required_space;
887 | }
888 | }
889 |
890 | var vertical_quad_space_pixels: u32 = (available_screen_dimensions.height - (outer_margin_pixels * 2)) - dimensions_pixels.height;
891 | var vertical_count: u32 = 1;
892 |
893 | {
894 | const required_space = dimensions_pixels.height + inner_margin_pixels;
895 | while(vertical_quad_space_pixels >= required_space) {
896 | vertical_count += 1;
897 | vertical_quad_space_pixels -= required_space;
898 | }
899 | }
900 |
901 | const y_offset_window_decorations: u32 = if(draw_window_decorations_requested) window_decorations.height_pixels else 0;
902 |
903 | const x_begin_pixels = outer_margin_pixels + (horizonal_quad_space_pixels / 2);
904 | const y_begin_pixels = y_offset_window_decorations + outer_margin_pixels + (vertical_quad_space_pixels / 2);
905 |
906 | const x_begin = -1.0 + (@as(f32, @floatFromInt(x_begin_pixels * 2)) / @as(f32, @floatFromInt(screen_dimensions.width)));
907 | const y_begin = -1.0 + (@as(f32, @floatFromInt(y_begin_pixels * 2)) / @as(f32, @floatFromInt(screen_dimensions.height)));
908 |
909 | const stride_horizonal = @as(f32, @floatFromInt((dimensions_pixels.width + inner_margin_pixels) * 2)) / @as(f32, @floatFromInt(screen_dimensions.width));
910 | const stride_vertical = @as(f32, @floatFromInt((dimensions_pixels.height + inner_margin_pixels) * 2)) / @as(f32, @floatFromInt(screen_dimensions.height));
911 |
912 | var face_writer = quad_face_writer_pool.create(1, (vertices_range_size - 1) / @sizeOf(graphics.GenericVertex));
913 | if(draw_window_decorations_requested) {
914 | var faces = try face_writer.allocate(3);
915 | const window_decoration_height = @as(f32, @floatFromInt(window_decorations.height_pixels * 2)) / @as(f32, @floatFromInt(screen_dimensions.height));
916 | {
917 | //
918 | // Draw window decoration topbar background
919 | //
920 | const extent = geometry.Extent2D(f32) {
921 | .x = -1.0,
922 | .y = -1.0,
923 | .width = 2.0,
924 | .height = window_decoration_height,
925 | };
926 | faces[0] = graphics.generateQuadColored(graphics.GenericVertex, extent, window_decorations.color, .top_left);
927 | }
928 | {
929 | //
930 | // Draw exit button in window decoration topbar
931 | //
932 | std.debug.assert(window_decorations.exit_button.size_pixels <= window_decorations.height_pixels);
933 | const screen_icon_dimensions = geometry.Dimensions2D(f32) {
934 | .width = @as(f32, @floatFromInt(window_decorations.exit_button.size_pixels * 2)) / @as(f32, @floatFromInt(screen_dimensions.width)),
935 | .height = @as(f32, @floatFromInt(window_decorations.exit_button.size_pixels * 2)) / @as(f32, @floatFromInt(screen_dimensions.height)),
936 | };
937 | const exit_button_outer_margin_pixels = @as(f32, @floatFromInt(window_decorations.height_pixels - window_decorations.exit_button.size_pixels)) / 2.0;
938 | const outer_margin_hor = exit_button_outer_margin_pixels * 2.0 / @as(f32, @floatFromInt(screen_dimensions.width));
939 | const outer_margin_ver = exit_button_outer_margin_pixels * 2.0 / @as(f32, @floatFromInt(screen_dimensions.height));
940 | const texture_coordinates = iconTextureLookup(.close);
941 | const texture_extent = geometry.Extent2D(f32) {
942 | .x = texture_coordinates.x,
943 | .y = texture_coordinates.y,
944 | .width = @as(f32, @floatFromInt(icon_dimensions.width)) / @as(f32, @floatFromInt(texture_layer_dimensions.width)),
945 | .height = @as(f32, @floatFromInt(icon_dimensions.height)) / @as(f32, @floatFromInt(texture_layer_dimensions.height)),
946 | };
947 | const extent = geometry.Extent2D(f32) {
948 | .x = 1.0 - (outer_margin_hor + screen_icon_dimensions.width),
949 | .y = -1.0 + outer_margin_ver,
950 | .width = screen_icon_dimensions.width,
951 | .height = screen_icon_dimensions.height,
952 | };
953 | faces[1] = graphics.generateQuadColored(graphics.GenericVertex, extent, window_decorations.color, .top_left);
954 | faces[2] = graphics.generateTexturedQuad(graphics.GenericVertex, extent, texture_extent, .top_left);
955 |
956 | // TODO: Update on screen size change
957 | const exit_button_extent_outer_margin = @divExact(window_decorations.height_pixels - window_decorations.exit_button.size_pixels, 2);
958 | exit_button_extent = geometry.Extent2D(u16) { // Top left anchor
959 | .x = screen_dimensions.width - (window_decorations.exit_button.size_pixels + exit_button_extent_outer_margin),
960 | .y = screen_dimensions.height - (window_decorations.exit_button.size_pixels + exit_button_extent_outer_margin),
961 | .width = window_decorations.exit_button.size_pixels,
962 | .height = window_decorations.exit_button.size_pixels,
963 | };
964 | exit_button_background_quad = &faces[1];
965 | }
966 | }
967 | var faces = try face_writer.allocate(horizonal_count * vertical_count);
968 | var horizonal_i: u32 = 0;
969 | while(horizonal_i < horizonal_count) : (horizonal_i += 1) {
970 | var vertical_i: u32 = 0;
971 | while(vertical_i < vertical_count) : (vertical_i += 1) {
972 | const extent = geometry.Extent2D(f32) {
973 | .x = x_begin + (stride_horizonal * @as(f32, @floatFromInt(horizonal_i))),
974 | .y = y_begin + (stride_vertical * @as(f32, @floatFromInt(vertical_i))),
975 | .width = @as(f32, @floatFromInt(dimensions_pixels.width)) / @as(f32, @floatFromInt(screen_dimensions.width)) * 2.0,
976 | .height = @as(f32, @floatFromInt(dimensions_pixels.height)) / @as(f32, @floatFromInt(screen_dimensions.height)) * 2.0,
977 | };
978 | const face_index = horizonal_i + (vertical_i * horizonal_count);
979 | const texture_coordinates = iconTextureLookup(@enumFromInt(face_index % icon_path_list.len));
980 | const texture_extent = geometry.Extent2D(f32) {
981 | .x = texture_coordinates.x,
982 | .y = texture_coordinates.y,
983 | .width = @as(f32, @floatFromInt(icon_dimensions.width)) / @as(f32, @floatFromInt(texture_layer_dimensions.width)),
984 | .height = @as(f32, @floatFromInt(icon_dimensions.height)) / @as(f32, @floatFromInt(texture_layer_dimensions.height)),
985 | };
986 | faces[face_index] = graphics.generateTexturedQuad(graphics.GenericVertex, extent, texture_extent, .top_left);
987 | }
988 | }
989 | vertex_buffer_quad_count = 1 + (horizonal_count * vertical_count);
990 | if(draw_window_decorations_requested) {
991 | vertex_buffer_quad_count += 3;
992 | }
993 | }
994 |
995 | fn setup(allocator: std.mem.Allocator, app: *GraphicsContext) !void {
996 | try waylandSetup();
997 |
998 | const vulkan_lib_symbol = comptime switch(builtin.os.tag) {
999 | .windows => "vulkan-1.dll",
1000 | .macos => "libvulkan.1.dylib",
1001 | .netbsd, .openbsd => "libvulkan.so",
1002 | else => "libvulkan.so.1",
1003 | };
1004 |
1005 | if (clib.dlopen(vulkan_lib_symbol, clib.RTLD_NOW)) |vulkan_loader| {
1006 | const vk_get_instance_proc_addr_fn_opt: ?*const fn (instance: vk.Instance, procname: [*:0]const u8) vk.PfnVoidFunction = @ptrCast(clib.dlsym(vulkan_loader, "vkGetInstanceProcAddr"));
1007 | if (vk_get_instance_proc_addr_fn_opt) |vk_get_instance_proc_addr_fn| {
1008 | vkGetInstanceProcAddr = vk_get_instance_proc_addr_fn;
1009 | app.base_dispatch = try vulkan_config.BaseDispatch.load(vkGetInstanceProcAddr);
1010 | } else {
1011 | std.log.err("Failed to load vkGetInstanceProcAddr function from vulkan loader", .{});
1012 | return error.FailedToGetVulkanSymbol;
1013 | }
1014 | } else {
1015 | std.log.err("Failed to load vulkan loader (libvulkan.so)", .{});
1016 | return error.FailedToGetVulkanSymbol;
1017 | }
1018 |
1019 | app.base_dispatch = try vulkan_config.BaseDispatch.load(vkGetInstanceProcAddr);
1020 |
1021 | app.instance = try app.base_dispatch.createInstance(&vk.InstanceCreateInfo{
1022 | .p_application_info = &vk.ApplicationInfo{
1023 | .p_application_name = application_name,
1024 | .application_version = vulkan_application_version,
1025 | .p_engine_name = vulkan_engine_name,
1026 | .engine_version = vulkan_engine_version,
1027 | .api_version = vulkan_api_version,
1028 | },
1029 | .enabled_extension_count = surface_extensions.len,
1030 | .pp_enabled_extension_names = @ptrCast(&surface_extensions),
1031 | .enabled_layer_count = if (enable_validation_layers) validation_layers.len else 0,
1032 | .pp_enabled_layer_names = if (enable_validation_layers) &validation_layers else undefined,
1033 | .flags = .{},
1034 | }, null);
1035 |
1036 | app.instance_dispatch = try vulkan_config.InstanceDispatch.load(app.instance, vkGetInstanceProcAddr);
1037 | errdefer app.instance_dispatch.destroyInstance(app.instance, null);
1038 |
1039 | {
1040 | const wayland_surface_create_info = vk.WaylandSurfaceCreateInfoKHR{
1041 | .display = @ptrCast(wayland_client.display),
1042 | .surface = @ptrCast(wayland_client.surface),
1043 | .flags = .{},
1044 | };
1045 |
1046 | app.surface = try app.instance_dispatch.createWaylandSurfaceKHR(
1047 | app.instance,
1048 | &wayland_surface_create_info,
1049 | null,
1050 | );
1051 | }
1052 | errdefer app.instance_dispatch.destroySurfaceKHR(app.instance, app.surface, null);
1053 |
1054 | // Find a suitable physical device (GPU/APU) to use
1055 | // Criteria:
1056 | // 1. Supports defined list of device extensions. See `device_extensions` above
1057 | // 2. Has a graphics queue that supports presentation on our selected surface
1058 | const best_physical_device = outer: {
1059 | const physical_devices = blk: {
1060 | var device_count: u32 = 0;
1061 | if(.success != (try app.instance_dispatch.enumeratePhysicalDevices(app.instance, &device_count, null))) {
1062 | std.log.warn("Failed to query physical device count", .{});
1063 | return error.PhysicalDeviceQueryFailure;
1064 | }
1065 |
1066 | if (device_count == 0) {
1067 | std.log.warn("No physical devices found", .{});
1068 | return error.NoDevicesFound;
1069 | }
1070 |
1071 | const devices = try allocator.alloc(vk.PhysicalDevice, device_count);
1072 | _ = try app.instance_dispatch.enumeratePhysicalDevices(app.instance, &device_count, devices.ptr);
1073 |
1074 | break :blk devices;
1075 | };
1076 | defer allocator.free(physical_devices);
1077 |
1078 | for (physical_devices, 0..) |physical_device, physical_device_i| {
1079 |
1080 | std.log.info("Physical vulkan devices found: {d}", .{physical_devices.len});
1081 |
1082 | const device_supports_extensions = blk: {
1083 | var extension_count: u32 = undefined;
1084 | if(.success != (try app.instance_dispatch.enumerateDeviceExtensionProperties(physical_device, null, &extension_count, null))) {
1085 | std.log.warn("Failed to get device extension property count for physical device index {d}", .{physical_device_i});
1086 | continue;
1087 | }
1088 |
1089 | const extensions = try allocator.alloc(vk.ExtensionProperties, extension_count);
1090 | defer allocator.free(extensions);
1091 |
1092 | if(.success != (try app.instance_dispatch.enumerateDeviceExtensionProperties(physical_device, null, &extension_count, extensions.ptr))) {
1093 | std.log.warn("Failed to load device extension properties for physical device index {d}", .{physical_device_i});
1094 | continue;
1095 | }
1096 |
1097 | dev_extensions: for (device_extensions) |requested_extension| {
1098 | for (extensions) |available_extension| {
1099 | // NOTE: We are relying on device_extensions to only contain c strings up to 255 charactors
1100 | // available_extension.extension_name will always be a null terminated string in a 256 char buffer
1101 | // https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VK_MAX_EXTENSION_NAME_SIZE.html
1102 | if(std.mem.orderZ(u8, requested_extension, @as([*:0]const u8, @ptrCast(&available_extension.extension_name))) == .eq) {
1103 | continue :dev_extensions;
1104 | }
1105 | }
1106 | break :blk false;
1107 | }
1108 | break :blk true;
1109 | };
1110 |
1111 | if(!device_supports_extensions) {
1112 | continue;
1113 | }
1114 |
1115 | var queue_family_count: u32 = 0;
1116 | app.instance_dispatch.getPhysicalDeviceQueueFamilyProperties(physical_device, &queue_family_count, null);
1117 |
1118 | if (queue_family_count == 0) {
1119 | continue;
1120 | }
1121 |
1122 | const max_family_queues: u32 = 16;
1123 | if (queue_family_count > max_family_queues) {
1124 | std.log.warn("Some family queues for selected device ignored", .{});
1125 | }
1126 |
1127 | var queue_families: [max_family_queues]vk.QueueFamilyProperties = undefined;
1128 | app.instance_dispatch.getPhysicalDeviceQueueFamilyProperties(physical_device, &queue_family_count, &queue_families);
1129 |
1130 | std.debug.print("** Queue Families found on device **\n\n", .{});
1131 | printVulkanQueueFamilies(queue_families[0..queue_family_count], 0);
1132 |
1133 | for(queue_families[0..queue_family_count], 0..) |queue_family, queue_family_i| {
1134 | if (queue_family.queue_count <= 0) {
1135 | continue;
1136 | }
1137 | if (queue_family.queue_flags.graphics_bit) {
1138 | const present_support = try app.instance_dispatch.getPhysicalDeviceSurfaceSupportKHR(
1139 | physical_device,
1140 | @intCast(queue_family_i),
1141 | app.surface,
1142 | );
1143 | if (present_support != 0) {
1144 | app.graphics_present_queue_index = @intCast(queue_family_i);
1145 | break :outer physical_device;
1146 | }
1147 | }
1148 | }
1149 | // If we reach here, we couldn't find a suitable present_queue an will
1150 | // continue to the next device
1151 | }
1152 | break :outer null;
1153 | };
1154 |
1155 | if (best_physical_device) |physical_device| {
1156 | app.physical_device = physical_device;
1157 | } else return error.NoSuitablePhysicalDevice;
1158 |
1159 | {
1160 | const device_create_info = vk.DeviceCreateInfo{
1161 | .queue_create_info_count = 1,
1162 | .p_queue_create_infos = @ptrCast(&vk.DeviceQueueCreateInfo{
1163 | .queue_family_index = app.graphics_present_queue_index,
1164 | .queue_count = 1,
1165 | .p_queue_priorities = &[1]f32{1.0},
1166 | .flags = .{},
1167 | }),
1168 | .p_enabled_features = &vulkan_config.enabled_device_features,
1169 | .enabled_extension_count = device_extensions.len,
1170 | .pp_enabled_extension_names = &device_extensions,
1171 | .enabled_layer_count = if (enable_validation_layers) validation_layers.len else 0,
1172 | .pp_enabled_layer_names = if (enable_validation_layers) &validation_layers else undefined,
1173 | .flags = .{},
1174 | };
1175 |
1176 | app.logical_device = try app.instance_dispatch.createDevice(
1177 | app.physical_device,
1178 | &device_create_info,
1179 | null,
1180 | );
1181 | }
1182 |
1183 | app.device_dispatch = try vulkan_config.DeviceDispatch.load(
1184 | app.logical_device,
1185 | app.instance_dispatch.dispatch.vkGetDeviceProcAddr,
1186 | );
1187 | app.graphics_present_queue = app.device_dispatch.getDeviceQueue(
1188 | app.logical_device,
1189 | app.graphics_present_queue_index,
1190 | 0,
1191 | );
1192 |
1193 | // Query and select appropriate surface format for swapchain
1194 | if(try selectSurfaceFormat(allocator, app.*, .srgb_nonlinear_khr, .b8g8r8a8_unorm)) |surface_format| {
1195 | app.surface_format = surface_format;
1196 | } else {
1197 | return error.RequiredSurfaceFormatUnavailable;
1198 | }
1199 |
1200 | const mesh_memory_index: u32 = blk: {
1201 | // Find the best memory type for storing mesh + texture data
1202 | // Requirements:
1203 | // - Sufficient space (20mib)
1204 | // - Host visible (Host refers to CPU. Allows for direct access without needing DMA)
1205 | // Preferable
1206 | // - Device local (Memory on the GPU / APU)
1207 |
1208 | const memory_properties = app.instance_dispatch.getPhysicalDeviceMemoryProperties(app.physical_device);
1209 | if(print_vulkan_objects.memory_type_all) {
1210 | std.debug.print("\n** Memory heaps found on system **\n\n", .{});
1211 | printVulkanMemoryHeaps(memory_properties, 0);
1212 | std.debug.print("\n", .{});
1213 | }
1214 |
1215 | const kib: u32 = 1024;
1216 | const mib: u32 = kib * 1024;
1217 | const minimum_space_required: u32 = mib * 20;
1218 |
1219 | var memory_type_index: u32 = 0;
1220 | const memory_type_count = memory_properties.memory_type_count;
1221 |
1222 | var suitable_memory_type_index_opt: ?u32 = null;
1223 |
1224 | while (memory_type_index < memory_type_count) : (memory_type_index += 1) {
1225 | const memory_entry = memory_properties.memory_types[memory_type_index];
1226 | const heap_index = memory_entry.heap_index;
1227 |
1228 | if (heap_index == memory_properties.memory_heap_count) {
1229 | std.log.warn("Invalid heap index {d} for memory type at index {d}. Skipping", .{ heap_index, memory_type_index });
1230 | continue;
1231 | }
1232 |
1233 | const heap_size = memory_properties.memory_heaps[heap_index].size;
1234 |
1235 | if (heap_size < minimum_space_required) {
1236 | continue;
1237 | }
1238 |
1239 | const memory_flags = memory_entry.property_flags;
1240 | if (memory_flags.host_visible_bit) {
1241 | suitable_memory_type_index_opt = memory_type_index;
1242 | if (memory_flags.device_local_bit) {
1243 | std.log.info("Selected memory for mesh buffer: Heap index ({d}) Memory index ({d})", .{heap_index, memory_type_index});
1244 | break :blk memory_type_index;
1245 | }
1246 | }
1247 | }
1248 |
1249 | if (suitable_memory_type_index_opt) |suitable_memory_type_index| {
1250 | break :blk suitable_memory_type_index;
1251 | }
1252 |
1253 | return error.NoValidVulkanMemoryTypes;
1254 | };
1255 |
1256 | {
1257 | const image_create_info = vk.ImageCreateInfo{
1258 | .flags = .{},
1259 | .image_type = .@"2d",
1260 | .format = .r32g32b32a32_sfloat,
1261 | .tiling = .linear,
1262 | .extent = vk.Extent3D{
1263 | .width = texture_layer_dimensions.width,
1264 | .height = texture_layer_dimensions.height,
1265 | .depth = 1,
1266 | },
1267 | .mip_levels = 1,
1268 | .array_layers = 1,
1269 | .initial_layout = .@"undefined",
1270 | .usage = .{ .transfer_dst_bit = true, .sampled_bit = true },
1271 | .samples = .{ .@"1_bit" = true },
1272 | .sharing_mode = .exclusive,
1273 | .queue_family_index_count = 0,
1274 | .p_queue_family_indices = undefined,
1275 | };
1276 |
1277 | texture_image = try app.device_dispatch.createImage(app.logical_device, &image_create_info, null);
1278 | }
1279 |
1280 | const texture_memory_requirements = app.device_dispatch.getImageMemoryRequirements(app.logical_device, texture_image);
1281 |
1282 | app.image_memory = try app.device_dispatch.allocateMemory(app.logical_device, &vk.MemoryAllocateInfo{
1283 | .allocation_size = texture_memory_requirements.size,
1284 | .memory_type_index = mesh_memory_index,
1285 | }, null);
1286 |
1287 | try app.device_dispatch.bindImageMemory(app.logical_device, texture_image, app.image_memory, 0);
1288 |
1289 | const command_pool = try app.device_dispatch.createCommandPool(app.logical_device, &vk.CommandPoolCreateInfo{
1290 | .queue_family_index = app.graphics_present_queue_index,
1291 | .flags = .{},
1292 | }, null);
1293 | // TODO: Just use app.command_pool that get's setup below
1294 | defer app.device_dispatch.destroyCommandPool(app.logical_device, command_pool, null);
1295 |
1296 | var command_buffer: vk.CommandBuffer = undefined;
1297 | {
1298 | const comment_buffer_alloc_info = vk.CommandBufferAllocateInfo{
1299 | .level = .primary,
1300 | .command_pool = command_pool,
1301 | .command_buffer_count = 1,
1302 | };
1303 | try app.device_dispatch.allocateCommandBuffers(
1304 | app.logical_device,
1305 | &comment_buffer_alloc_info,
1306 | @ptrCast(&command_buffer)
1307 | );
1308 | }
1309 |
1310 | try app.device_dispatch.beginCommandBuffer(command_buffer, &vk.CommandBufferBeginInfo{
1311 | .flags = .{ .one_time_submit_bit = true },
1312 | .p_inheritance_info = null,
1313 | });
1314 |
1315 | // Just putting this code here for reference
1316 | // Currently I'm using host visible memory so a staging buffer is not required
1317 |
1318 | // TODO: Using the staging buffer will cause the texture_memory map to point to the staging buffer
1319 | // Instead of the uploaded memory
1320 | const is_staging_buffer_required: bool = false;
1321 | if (is_staging_buffer_required) {
1322 | const staging_buffer = blk: {
1323 | const create_buffer_info = vk.BufferCreateInfo{
1324 | .flags = .{},
1325 | .size = texture_size_bytes * 2,
1326 | .usage = .{ .transfer_src_bit = true },
1327 | .sharing_mode = .exclusive,
1328 | .queue_family_index_count = 0,
1329 | .p_queue_family_indices = undefined,
1330 | };
1331 | break :blk try app.device_dispatch.createBuffer(app.logical_device, &create_buffer_info, null);
1332 | };
1333 |
1334 | const staging_memory = blk: {
1335 | const staging_memory_alloc = vk.MemoryAllocateInfo{
1336 | .allocation_size = texture_size_bytes * 2, // x2 because we have two array layers
1337 | .memory_type_index = mesh_memory_index, // TODO:
1338 | };
1339 | break :blk try app.device_dispatch.allocateMemory(app.logical_device, &staging_memory_alloc, null);
1340 | };
1341 |
1342 | try app.device_dispatch.bindBufferMemory(app.logical_device, staging_buffer, staging_memory, 0);
1343 | {
1344 | const mapped_memory_ptr = (try app.device_dispatch.mapMemory(app.logical_device, staging_memory, 0, staging_memory, .{})).?;
1345 | texture_memory_map = @ptrCast(@alignCast(mapped_memory_ptr));
1346 | }
1347 |
1348 | // TODO: texture_size_bytes * 2
1349 | // if (.success != vk.vkMapMemory(app.logical_device, staging_memory, 0, texture_layer_size * 2, 0, @ptrCast(?**anyopaque, &texture_memory_map))) {
1350 | // return error.MapMemoryFailed;
1351 | // }
1352 |
1353 | // Copy our second image to same memory
1354 | // TODO: Fix data layout access
1355 | // @memcpy(texture_memory_map, @ptrCast([*]u8, glyph_set.image), texture_layer_size);
1356 | // @memcpy(texture_memory_map + texture_layer_size, @ptrCast([*]u8, texture_layer), texture_layer_size);
1357 |
1358 | // No need to unmap memory
1359 | // vk.vkUnmapMemory(app.logical_device, staging_memory);
1360 |
1361 | {
1362 | const barrier = [_]vk.ImageMemoryBarrier{
1363 | .{
1364 | .src_access_mask = .{},
1365 | .dst_access_mask = .{ .transfer_write_bit = true },
1366 | .old_layout = .@"undefined",
1367 | .new_layout = .transfer_dst_optimal,
1368 | .src_queue_family_index = vk.QUEUE_FAMILY_IGNORED,
1369 | .dst_queue_family_index = vk.QUEUE_FAMILY_IGNORED,
1370 | .image = texture_image,
1371 | .subresource_range = .{
1372 | .aspect_mask = .{ .color_bit = true },
1373 | .base_mip_level = 0,
1374 | .level_count = 1,
1375 | .base_array_layer = 0,
1376 | .layer_count = 2,
1377 | },
1378 | },
1379 | };
1380 |
1381 | const src_stage = vk.PipelineStageFlags{ .top_of_pipe_bit = true };
1382 | const dst_stage = vk.PipelineStageFlags{ .transfer_bit = true };
1383 | app.device_dispatch.cmdPipelineBarrier(command_buffer, src_stage, dst_stage, .{}, 0, undefined, 0, undefined, 1, &barrier);
1384 | }
1385 |
1386 | const regions = [_]vk.BufferImageCopy{
1387 | .{
1388 | .buffer_offset = 0,
1389 | .buffer_row_length = 0,
1390 | .buffer_image_height = 0,
1391 | .image_subresource = .{
1392 | .aspect_mask = .{ .color_bit = true },
1393 | .mip_level = 0,
1394 | .base_array_layer = 0,
1395 | .layer_count = 1,
1396 | },
1397 | .image_offset = .{ .x = 0, .y = 0, .z = 0 },
1398 | .image_extent = .{
1399 | .width = texture_layer_dimensions.width,
1400 | .height = texture_layer_dimensions.height,
1401 | .depth = 1,
1402 | }
1403 | },
1404 | .{
1405 | .buffer_offset = texture_size_bytes,
1406 | .buffer_row_length = 0,
1407 | .buffer_image_height = 0,
1408 | .image_subresource = .{
1409 | .aspect_mask = .{ .color_bit = true },
1410 | .mip_level = 0,
1411 | .base_array_layer = 1,
1412 | .layer_count = 1,
1413 | },
1414 | .image_offset = .{ .x = 0, .y = 0, .z = 0 },
1415 | .image_extent = .{
1416 | .width = texture_layer_dimensions.width,
1417 | .height = texture_layer_dimensions.height,
1418 | .depth = 1,
1419 | }
1420 | },
1421 | };
1422 |
1423 | _ = app.device_dispatch.cmdCopyBufferToImage(command_buffer, staging_buffer, texture_image, .transfer_dst_optimal, 2, ®ions);
1424 | } else {
1425 | std.debug.assert(texture_layer_size <= texture_memory_requirements.size);
1426 | std.debug.assert(texture_memory_requirements.alignment >= 16);
1427 | {
1428 | const mapped_memory_ptr = (try app.device_dispatch.mapMemory(app.logical_device, app.image_memory, 0, texture_layer_size, .{})).?;
1429 | texture_memory_map = @ptrCast(@alignCast(mapped_memory_ptr));
1430 | }
1431 |
1432 | // TODO: This could be done on another thread
1433 | for(icon_path_list, 0..) |icon_path, icon_path_i| {
1434 | // TODO: Create an arena / allocator of a fixed size that can be reused here.
1435 | var icon_image = try img.Image.fromFilePath(allocator, icon_path);
1436 | defer icon_image.deinit();
1437 |
1438 | const icon_type: IconType = @enumFromInt(icon_path_i);
1439 |
1440 | if(icon_image.width != icon_image.width or icon_image.height != icon_image.height) {
1441 | std.log.err("Icon image for icon '{}' has unexpected dimensions."
1442 | ++ "Icon assets may have gotten corrupted", .{icon_type});
1443 | return error.AssetIconsDimensionsMismatch;
1444 | }
1445 |
1446 | const source_pixels = switch(icon_image.pixels) {
1447 | .rgba32 => |pixels| pixels,
1448 | else => {
1449 | std.log.err("Icon images are expected to be in rgba32. Icon '{}' may have gotten corrupted", .{icon_type});
1450 | return error.AssetIconsFormatInvalid;
1451 | },
1452 | };
1453 |
1454 | if(source_pixels.len != (icon_image.width * icon_image.height)) {
1455 | std.log.err("Loaded image for icon '{}' has an unexpected number of pixels."
1456 | ++ "This is a bug in the application", .{icon_type});
1457 | return error.AssetIconsMalformed;
1458 | }
1459 |
1460 | std.debug.assert(source_pixels.len == icon_image.width * icon_image.height);
1461 |
1462 | const x = @as(u32, @intCast(icon_path_i)) % icon_texture_row_count;
1463 | const y = @as(u32, @intCast(icon_path_i)) / icon_texture_row_count;
1464 | const dst_offset_coords = geometry.Coordinates2D(u32) {
1465 | .x = x * icon_dimensions.width,
1466 | .y = y * icon_dimensions.height,
1467 | };
1468 | var src_y: u32 = 0;
1469 | while(src_y < icon_dimensions.height) : (src_y += 1) {
1470 | var src_x: u32 = 0;
1471 | while(src_x < icon_dimensions.width) : (src_x += 1) {
1472 | const src_index = src_x + (src_y * icon_dimensions.width);
1473 | const dst_index = (dst_offset_coords.x + src_x) + ((dst_offset_coords.y + src_y) * texture_layer_dimensions.width);
1474 |
1475 | texture_memory_map[dst_index].r = icon_color.r;
1476 | texture_memory_map[dst_index].g = icon_color.g;
1477 | texture_memory_map[dst_index].b = icon_color.b;
1478 | texture_memory_map[dst_index].a = @as(f32, @floatFromInt(source_pixels[src_index].a)) / 255.0;
1479 | }
1480 | }
1481 | }
1482 |
1483 | // Not sure if this is a hack, but because we multiply the texture sample by the
1484 | // color in the fragment shader, we need pixel in the texture that we known will return 1.0
1485 | // Here we're setting the last pixel to 1.0, which corresponds to a texture mapping of 1.0, 1.0
1486 | const last_index: usize = (@as(usize, @intCast(texture_layer_dimensions.width)) * texture_layer_dimensions.height) - 1;
1487 | texture_memory_map[last_index].r = 1.0;
1488 | texture_memory_map[last_index].g = 1.0;
1489 | texture_memory_map[last_index].b = 1.0;
1490 | texture_memory_map[last_index].a = 1.0;
1491 | }
1492 |
1493 | // Regardless of whether a staging buffer was used, and the type of memory that backs the texture
1494 | // It is neccessary to transition to image layout to SHADER_OPTIMAL
1495 | const barrier = [_]vk.ImageMemoryBarrier{
1496 | .{
1497 | .src_access_mask = .{},
1498 | .dst_access_mask = .{ .shader_read_bit = true },
1499 | .old_layout = .@"undefined",
1500 | .new_layout = .shader_read_only_optimal,
1501 | .src_queue_family_index = vk.QUEUE_FAMILY_IGNORED,
1502 | .dst_queue_family_index = vk.QUEUE_FAMILY_IGNORED,
1503 | .image = texture_image,
1504 | .subresource_range = .{
1505 | .aspect_mask = .{ .color_bit = true },
1506 | .base_mip_level = 0,
1507 | .level_count = 1,
1508 | .base_array_layer = 0,
1509 | .layer_count = 1,
1510 | },
1511 | },
1512 | };
1513 |
1514 | {
1515 | const src_stage = vk.PipelineStageFlags{ .top_of_pipe_bit = true };
1516 | const dst_stage = vk.PipelineStageFlags{ .fragment_shader_bit = true };
1517 | const dependency_flags = vk.DependencyFlags{};
1518 | app.device_dispatch.cmdPipelineBarrier(command_buffer, src_stage, dst_stage, dependency_flags, 0, undefined, 0, undefined, 1, &barrier);
1519 | }
1520 |
1521 | try app.device_dispatch.endCommandBuffer(command_buffer);
1522 |
1523 | const submit_command_infos = [_]vk.SubmitInfo{.{
1524 | .wait_semaphore_count = 0,
1525 | .p_wait_semaphores = undefined,
1526 | .p_wait_dst_stage_mask = undefined,
1527 | .command_buffer_count = 1,
1528 | .p_command_buffers = @ptrCast(&command_buffer),
1529 | .signal_semaphore_count = 0,
1530 | .p_signal_semaphores = undefined,
1531 | }};
1532 |
1533 | const barrier_fence: vk.Fence = try app.device_dispatch.createFence(app.logical_device, &.{}, null);
1534 | try app.device_dispatch.queueSubmit(app.graphics_present_queue, 1, &submit_command_infos, barrier_fence);
1535 |
1536 | texture_image_view = try app.device_dispatch.createImageView(app.logical_device, &vk.ImageViewCreateInfo{
1537 | .flags = .{},
1538 | .image = texture_image,
1539 | .view_type = .@"2d_array",
1540 | .format = .r32g32b32a32_sfloat,
1541 | .subresource_range = .{
1542 | .aspect_mask = .{ .color_bit = true },
1543 | .base_mip_level = 0,
1544 | .level_count = 1,
1545 | .base_array_layer = 0,
1546 | .layer_count = 1,
1547 | },
1548 | .components = .{ .r = .identity, .g = .identity, .b = .identity, .a = .identity },
1549 | }, null);
1550 |
1551 | const surface_capabilities = try app.instance_dispatch.getPhysicalDeviceSurfaceCapabilitiesKHR(app.physical_device, app.surface);
1552 |
1553 | if(print_vulkan_objects.surface_abilties) {
1554 | std.debug.print("** Selected surface capabilites **\n\n", .{});
1555 | printSurfaceCapabilities(surface_capabilities, 1);
1556 | std.debug.print("\n", .{});
1557 | }
1558 |
1559 | if(transparancy_enabled) {
1560 | // Check to see if the compositor supports transparent windows and what
1561 | // transparency mode needs to be set when creating the swapchain
1562 | const supported = surface_capabilities.supported_composite_alpha;
1563 | if (supported.pre_multiplied_bit_khr) {
1564 | alpha_mode = .{ .pre_multiplied_bit_khr = true };
1565 | } else if (supported.post_multiplied_bit_khr) {
1566 | alpha_mode = .{ .post_multiplied_bit_khr = true };
1567 | } else if (supported.inherit_bit_khr) {
1568 | alpha_mode = .{ .inherit_bit_khr = true };
1569 | } else {
1570 | std.log.info("Alpha windows not supported", .{});
1571 | }
1572 | }
1573 |
1574 | if (surface_capabilities.current_extent.width == 0xFFFFFFFF or surface_capabilities.current_extent.height == 0xFFFFFFFF) {
1575 | app.swapchain_extent.width = screen_dimensions.width;
1576 | app.swapchain_extent.height = screen_dimensions.height;
1577 | }
1578 |
1579 | std.debug.assert(app.swapchain_extent.width >= surface_capabilities.min_image_extent.width);
1580 | std.debug.assert(app.swapchain_extent.height >= surface_capabilities.min_image_extent.height);
1581 |
1582 | std.debug.assert(app.swapchain_extent.width <= surface_capabilities.max_image_extent.width);
1583 | std.debug.assert(app.swapchain_extent.height <= surface_capabilities.max_image_extent.height);
1584 |
1585 | app.swapchain_min_image_count = surface_capabilities.min_image_count + 1;
1586 |
1587 | // TODO: Perhaps more flexibily should be allowed here. I'm unsure if an application is
1588 | // supposed to match the rotation of the system / monitor, but I would assume not..
1589 | // It is also possible that the inherit_bit_khr bit would be set in place of identity_bit_khr
1590 | if(surface_capabilities.current_transform.identity_bit_khr == false) {
1591 | std.log.err("Selected surface does not have the option to leave framebuffer image untransformed." ++
1592 | "This is likely a vulkan bug.", .{});
1593 | return error.VulkanSurfaceTransformInvalid;
1594 | }
1595 |
1596 | app.swapchain = try app.device_dispatch.createSwapchainKHR(app.logical_device, &vk.SwapchainCreateInfoKHR{
1597 | .surface = app.surface,
1598 | .min_image_count = app.swapchain_min_image_count,
1599 | .image_format = app.surface_format.format,
1600 | .image_color_space = app.surface_format.color_space,
1601 | .image_extent = app.swapchain_extent,
1602 | .image_array_layers = 1,
1603 | .image_usage = .{ .color_attachment_bit = true },
1604 | .image_sharing_mode = .exclusive,
1605 | // NOTE: Only valid when `image_sharing_mode` is CONCURRENT
1606 | // https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VkSwapchainCreateInfoKHR.html
1607 | .queue_family_index_count = 0,
1608 | .p_queue_family_indices = undefined,
1609 | .pre_transform = .{ .identity_bit_khr = true },
1610 | .composite_alpha = alpha_mode,
1611 | // NOTE: FIFO_KHR is required to be available for all vulkan capable devices
1612 | // For that reason we don't need to query for it on our selected device
1613 | // https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VkPresentModeKHR.html
1614 | .present_mode = .fifo_khr,
1615 | .clipped = vk.TRUE,
1616 | .flags = .{},
1617 | .old_swapchain = .null_handle,
1618 | }, null);
1619 |
1620 | app.swapchain_images = blk: {
1621 | var image_count: u32 = undefined;
1622 | if (.success != (try app.device_dispatch.getSwapchainImagesKHR(app.logical_device, app.swapchain, &image_count, null))) {
1623 | return error.FailedToGetSwapchainImagesCount;
1624 | }
1625 |
1626 | const swapchain_images = try allocator.alloc(vk.Image, image_count);
1627 | if (.success != (try app.device_dispatch.getSwapchainImagesKHR(app.logical_device, app.swapchain, &image_count, swapchain_images.ptr))) {
1628 | return error.FailedToGetSwapchainImages;
1629 | }
1630 |
1631 | break :blk swapchain_images;
1632 | };
1633 |
1634 | app.swapchain_image_views = try allocator.alloc(vk.ImageView, app.swapchain_images.len);
1635 | try createSwapchainImageViews(app.*);
1636 |
1637 | std.debug.assert(vertices_range_index_begin + vertices_range_size <= memory_size);
1638 |
1639 | app.mesh_memory = try app.device_dispatch.allocateMemory(app.logical_device, &vk.MemoryAllocateInfo{
1640 | .allocation_size = memory_size,
1641 | .memory_type_index = mesh_memory_index,
1642 | }, null);
1643 |
1644 | {
1645 | const buffer_create_info = vk.BufferCreateInfo{
1646 | .size = vertices_range_size,
1647 | .usage = .{ .transfer_dst_bit = true, .vertex_buffer_bit = true },
1648 | .sharing_mode = .exclusive,
1649 | // NOTE: Only valid when `sharing_mode` is CONCURRENT
1650 | // https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VkBufferCreateInfo.html
1651 | .queue_family_index_count = 0,
1652 | .p_queue_family_indices = undefined,
1653 | .flags = .{},
1654 | };
1655 |
1656 | texture_vertices_buffer = try app.device_dispatch.createBuffer(app.logical_device, &buffer_create_info, null);
1657 | try app.device_dispatch.bindBufferMemory(app.logical_device, texture_vertices_buffer, app.mesh_memory, vertices_range_index_begin);
1658 | }
1659 |
1660 | {
1661 | const buffer_create_info = vk.BufferCreateInfo{
1662 | .size = indices_range_size,
1663 | .usage = .{ .transfer_dst_bit = true, .index_buffer_bit = true },
1664 | .sharing_mode = .exclusive,
1665 | // NOTE: Only valid when `sharing_mode` is CONCURRENT
1666 | // https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VkBufferCreateInfo.html
1667 | .queue_family_index_count = 0,
1668 | .p_queue_family_indices = undefined,
1669 | .flags = .{},
1670 | };
1671 |
1672 | texture_indices_buffer = try app.device_dispatch.createBuffer(app.logical_device, &buffer_create_info, null);
1673 | try app.device_dispatch.bindBufferMemory(app.logical_device, texture_indices_buffer, app.mesh_memory, indices_range_index_begin);
1674 | }
1675 |
1676 | mapped_device_memory = @ptrCast((try app.device_dispatch.mapMemory(app.logical_device, app.mesh_memory, 0, memory_size, .{})).?);
1677 |
1678 | {
1679 | // TODO: Cleanup alignCasts
1680 | const required_alignment = @alignOf(graphics.GenericVertex);
1681 | const vertices_addr: [*]align(required_alignment) u8 = @ptrCast(@alignCast(&mapped_device_memory[vertices_range_index_begin]));
1682 | background_quad = @ptrCast(@alignCast(&vertices_addr[0]));
1683 | background_quad.* = graphics.generateQuadColored(graphics.GenericVertex, full_screen_extent, background_color_b[0].toRGBA(), .top_left);
1684 | const vertices_quad_size: u32 = vertices_range_size / @sizeOf(graphics.GenericVertex);
1685 | quad_face_writer_pool = QuadFaceWriterPool(graphics.GenericVertex).initialize(vertices_addr, vertices_quad_size);
1686 | }
1687 |
1688 | {
1689 | // We won't be reusing vertices except in making quads so we can pre-generate the entire indices buffer
1690 | var indices: [*] align(16)u16 = @ptrCast(@alignCast(&mapped_device_memory[indices_range_index_begin]));
1691 |
1692 | var j: u32 = 0;
1693 | while (j < (indices_range_count / 6)) : (j += 1) {
1694 | indices[j * 6 + 0] = @as(u16, @intCast(j * 4)) + 0; // Top left
1695 | indices[j * 6 + 1] = @as(u16, @intCast(j * 4)) + 1; // Top right
1696 | indices[j * 6 + 2] = @as(u16, @intCast(j * 4)) + 2; // Bottom right
1697 | indices[j * 6 + 3] = @as(u16, @intCast(j * 4)) + 0; // Top left
1698 | indices[j * 6 + 4] = @as(u16, @intCast(j * 4)) + 2; // Bottom right
1699 | indices[j * 6 + 5] = @as(u16, @intCast(j * 4)) + 3; // Bottom left
1700 | }
1701 | }
1702 |
1703 | {
1704 | const command_pool_create_info = vk.CommandPoolCreateInfo{
1705 | .queue_family_index = app.graphics_present_queue_index,
1706 | .flags = .{},
1707 | };
1708 |
1709 | app.command_pool = try app.device_dispatch.createCommandPool(app.logical_device, &command_pool_create_info, null);
1710 | }
1711 |
1712 | app.images_available = try allocator.alloc(vk.Semaphore, max_frames_in_flight);
1713 | app.renders_finished = try allocator.alloc(vk.Semaphore, max_frames_in_flight);
1714 | app.inflight_fences = try allocator.alloc(vk.Fence, max_frames_in_flight);
1715 |
1716 | const semaphore_create_info = vk.SemaphoreCreateInfo{
1717 | .flags = .{},
1718 | };
1719 |
1720 | const fence_create_info = vk.FenceCreateInfo{
1721 | .flags = .{ .signaled_bit = true },
1722 | };
1723 |
1724 | var i: u32 = 0;
1725 | while (i < max_frames_in_flight) {
1726 | app.images_available[i] = try app.device_dispatch.createSemaphore(app.logical_device, &semaphore_create_info, null);
1727 | app.renders_finished[i] = try app.device_dispatch.createSemaphore(app.logical_device, &semaphore_create_info, null);
1728 | app.inflight_fences[i] = try app.device_dispatch.createFence(app.logical_device, &fence_create_info, null);
1729 | i += 1;
1730 | }
1731 |
1732 | app.vertex_shader_module = try createVertexShaderModule(app.*);
1733 | app.fragment_shader_module = try createFragmentShaderModule(app.*);
1734 |
1735 | std.debug.assert(app.swapchain_images.len > 0);
1736 |
1737 | {
1738 | app.command_buffers = try allocator.alloc(vk.CommandBuffer, app.swapchain_images.len);
1739 | const command_buffer_allocate_info = vk.CommandBufferAllocateInfo{
1740 | .command_pool = app.command_pool,
1741 | .level = .primary,
1742 | .command_buffer_count = @intCast(app.command_buffers.len),
1743 | };
1744 | try app.device_dispatch.allocateCommandBuffers(app.logical_device, &command_buffer_allocate_info, app.command_buffers.ptr);
1745 | }
1746 |
1747 | app.render_pass = try createRenderPass(app.*);
1748 |
1749 | app.descriptor_set_layouts = try createDescriptorSetLayouts(allocator, app.*);
1750 | app.pipeline_layout = try createPipelineLayout(app.*, app.descriptor_set_layouts);
1751 | app.descriptor_pool = try createDescriptorPool(app.*);
1752 | app.descriptor_sets = try createDescriptorSets(allocator, app, app.descriptor_set_layouts);
1753 | app.graphics_pipeline = try createGraphicsPipeline(app.*, app.pipeline_layout, app.render_pass);
1754 | app.framebuffers = try createFramebuffers(allocator, app.*);
1755 |
1756 | _ = try app.device_dispatch.waitForFences(app.logical_device, 1, @ptrCast(&barrier_fence), vk.TRUE, std.math.maxInt(u64),);
1757 | app.device_dispatch.destroyFence(app.logical_device, barrier_fence, null);
1758 | }
1759 |
1760 | //
1761 | // 4. Wayland Types + Functions
1762 | //
1763 |
1764 | const WaylandClient = struct {
1765 | display: *wl.Display,
1766 | registry: *wl.Registry,
1767 | compositor: *wl.Compositor,
1768 | xdg_wm_base: *xdg.WmBase,
1769 | surface: *wl.Surface,
1770 | seat: *wl.Seat,
1771 | pointer: *wl.Pointer,
1772 | frame_callback: *wl.Callback,
1773 | xdg_toplevel: *xdg.Toplevel,
1774 | xdg_surface: *xdg.Surface,
1775 |
1776 | cursor_theme: *wl.CursorTheme,
1777 | cursor: *wl.Cursor,
1778 | cursor_surface: *wl.Surface,
1779 | xcursor: [:0]const u8,
1780 | cursor_shared_memory: *wl.Shm,
1781 | };
1782 |
1783 | const XCursor = struct {
1784 | const hidden = "hidden";
1785 | const left_ptr = "left_ptr";
1786 | const text = "text";
1787 | const xterm = "xterm";
1788 | const hand2 = "hand2";
1789 | const top_left_corner = "top_left_corner";
1790 | const top_right_corner = "top_right_corner";
1791 | const bottom_left_corner = "bottom_left_corner";
1792 | const bottom_right_corner = "bottom_right_corner";
1793 | const left_side = "left_side";
1794 | const right_side = "right_side";
1795 | const top_side = "top_side";
1796 | const bottom_side = "bottom_side";
1797 | };
1798 |
1799 | /// Wayland uses linux' input-event-codes for keys and buttons. When a mouse button is
1800 | /// clicked one of these will be sent with the event.
1801 | /// https://wayland-book.com/seat/pointer.html
1802 | /// https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h
1803 | const MouseButton = enum(c_int) {
1804 | left = 0x110,
1805 | right = 0x111,
1806 | middle = 0x112,
1807 | _
1808 | };
1809 |
1810 | fn xdgWmBaseListener(xdg_wm_base: *xdg.WmBase, event: xdg.WmBase.Event, _: *WaylandClient) void {
1811 | switch (event) {
1812 | .ping => |ping| {
1813 | xdg_wm_base.pong(ping.serial);
1814 | },
1815 | }
1816 | }
1817 |
1818 | fn xdgSurfaceListener(xdg_surface: *xdg.Surface, event: xdg.Surface.Event, surface: *wl.Surface) void {
1819 | switch (event) {
1820 | .configure => |configure| {
1821 | xdg_surface.ackConfigure(configure.serial);
1822 | surface.commit();
1823 | },
1824 | }
1825 | }
1826 |
1827 | fn xdgToplevelListener(_: *xdg.Toplevel, event: xdg.Toplevel.Event, close_requested: *bool) void {
1828 | switch (event) {
1829 | .configure => |configure| {
1830 | if (configure.width > 0 and configure.width != screen_dimensions.width) {
1831 | framebuffer_resized = true;
1832 | screen_dimensions.width = @intCast(configure.width);
1833 | }
1834 | if (configure.height > 0 and configure.height != screen_dimensions.height) {
1835 | framebuffer_resized = true;
1836 | screen_dimensions.height = @intCast(configure.height);
1837 | }
1838 | },
1839 | .close => close_requested.* = true,
1840 | }
1841 | }
1842 |
1843 | fn frameListener(callback: *wl.Callback, event: wl.Callback.Event, client: *WaylandClient) void {
1844 | switch (event) {
1845 | .done => {
1846 | is_render_requested = true;
1847 | callback.destroy();
1848 | client.frame_callback = client.surface.frame() catch |err| {
1849 | std.log.err("Failed to create new wayland frame -> {}", .{err});
1850 | return;
1851 | };
1852 | client.frame_callback.setListener(*WaylandClient, frameListener, client);
1853 | },
1854 | }
1855 | }
1856 |
1857 | fn shmListener(shm: *wl.Shm, event: wl.Shm.Event, client: *WaylandClient) void {
1858 | _ = client;
1859 | _ = shm;
1860 | switch(event) {
1861 | .format => |format| {
1862 | std.log.info("Shm foramt: {}", .{format});
1863 | },
1864 | }
1865 | }
1866 |
1867 | fn pointerListener(_: *wl.Pointer, event: wl.Pointer.Event, client: *WaylandClient) void {
1868 | switch (event) {
1869 | .enter => |enter| {
1870 | is_mouse_in_screen = true;
1871 | mouse_coordinates.x = enter.surface_x.toDouble();
1872 | mouse_coordinates.y = enter.surface_y.toDouble();
1873 |
1874 | //
1875 | // When mouse enters application surface, update the cursor image
1876 | //
1877 | const image = client.cursor.images[0];
1878 | const image_buffer = image.getBuffer() catch return;
1879 | client.cursor_surface.attach(image_buffer, 0, 0);
1880 | client.pointer.setCursor(enter.serial, client.cursor_surface, @intCast(image.hotspot_x), @intCast(image.hotspot_y));
1881 | client.cursor_surface.damageBuffer(0, 0, std.math.maxInt(i32), std.math.maxInt(i32));
1882 | client.cursor_surface.commit();
1883 | },
1884 | .leave => |leave| {
1885 | _ = leave;
1886 | is_mouse_in_screen = false;
1887 | },
1888 | .motion => |motion| {
1889 | mouse_coordinates.x = motion.surface_x.toDouble();
1890 | mouse_coordinates.y = motion.surface_y.toDouble();
1891 |
1892 | if(@as(u16, @intFromFloat(mouse_coordinates.y)) > screen_dimensions.height or @as(u16, @intFromFloat(mouse_coordinates.x)) > screen_dimensions.width) {
1893 | return;
1894 | }
1895 |
1896 | const end_x = exit_button_extent.x + exit_button_extent.width;
1897 | const end_y = exit_button_extent.y + exit_button_extent.height;
1898 | const mouse_x: u16 = @intFromFloat(mouse_coordinates.x);
1899 | const mouse_y = screen_dimensions.height - @as(u16, @intFromFloat(mouse_coordinates.y));
1900 | const is_within_bounds = (mouse_x >= exit_button_extent.x and mouse_y >= exit_button_extent.y and mouse_x <= end_x and mouse_y <= end_y);
1901 |
1902 | if(is_within_bounds and !exit_button_hovered) {
1903 | exit_button_background_quad[0].color = window_decorations.exit_button.color_hovered;
1904 | exit_button_background_quad[1].color = window_decorations.exit_button.color_hovered;
1905 | exit_button_background_quad[2].color = window_decorations.exit_button.color_hovered;
1906 | exit_button_background_quad[3].color = window_decorations.exit_button.color_hovered;
1907 | is_render_requested = true;
1908 | exit_button_hovered = true;
1909 | }
1910 |
1911 | if(!is_within_bounds and exit_button_hovered) {
1912 | exit_button_background_quad[0].color = window_decorations.color;
1913 | exit_button_background_quad[1].color = window_decorations.color;
1914 | exit_button_background_quad[2].color = window_decorations.color;
1915 | exit_button_background_quad[3].color = window_decorations.color;
1916 | is_render_requested = true;
1917 | exit_button_hovered = false;
1918 | }
1919 | },
1920 | .button => |button| {
1921 |
1922 | if(!is_mouse_in_screen) {
1923 | return;
1924 | }
1925 |
1926 | const mouse_button: MouseButton = @enumFromInt(button.button);
1927 | {
1928 | const mouse_x: u16 = @intFromFloat(mouse_coordinates.x);
1929 | const mouse_y: u16 = @intFromFloat(mouse_coordinates.y);
1930 | std.log.info("Mouse coords: {d}, {d}. Screen {d}, {d}", .{mouse_x, mouse_y, screen_dimensions.width, screen_dimensions.height});
1931 | if(mouse_x < 3 and mouse_y < 3) {
1932 | client.xdg_toplevel.resize(client.seat, button.serial, .bottom_left);
1933 | }
1934 |
1935 | const edge_threshold = 3;
1936 | const max_width = screen_dimensions.width - edge_threshold;
1937 | const max_height = screen_dimensions.height - edge_threshold;
1938 |
1939 | if(mouse_x < edge_threshold and mouse_y > max_height) {
1940 | client.xdg_toplevel.resize(client.seat, button.serial, .top_left);
1941 | return;
1942 | }
1943 |
1944 | if(mouse_x > max_width and mouse_y < edge_threshold) {
1945 | client.xdg_toplevel.resize(client.seat, button.serial, .bottom_right);
1946 | return;
1947 | }
1948 |
1949 | if(mouse_x > max_width and mouse_y > max_height) {
1950 | client.xdg_toplevel.resize(client.seat, button.serial, .bottom_right);
1951 | return;
1952 | }
1953 |
1954 | if(mouse_x < edge_threshold) {
1955 | client.xdg_toplevel.resize(client.seat, button.serial, .left);
1956 | return;
1957 | }
1958 |
1959 | if(mouse_x > max_width) {
1960 | client.xdg_toplevel.resize(client.seat, button.serial, .right);
1961 | return;
1962 | }
1963 |
1964 | if(mouse_y <= edge_threshold) {
1965 | client.xdg_toplevel.resize(client.seat, button.serial, .top);
1966 | return;
1967 | }
1968 |
1969 | if(mouse_y == max_height) {
1970 | client.xdg_toplevel.resize(client.seat, button.serial, .bottom);
1971 | return;
1972 | }
1973 | }
1974 |
1975 | if(@as(u16, @intFromFloat(mouse_coordinates.y)) > screen_dimensions.height or @as(u16, @intFromFloat(mouse_coordinates.x)) > screen_dimensions.width) {
1976 | return;
1977 | }
1978 |
1979 | if(draw_window_decorations_requested and mouse_button == .left) {
1980 | // Start interactive window move if mouse coordinates are in window decorations bounds
1981 | if(@as(u32, @intFromFloat(mouse_coordinates.y)) <= window_decorations.height_pixels) {
1982 | client.xdg_toplevel.move(client.seat, button.serial);
1983 | }
1984 | const end_x = exit_button_extent.x + exit_button_extent.width;
1985 | const end_y = exit_button_extent.y + exit_button_extent.height;
1986 | const mouse_x: u16 = @intFromFloat(mouse_coordinates.x);
1987 | const mouse_y: u16 = screen_dimensions.height - @as(u16, @intFromFloat(mouse_coordinates.y));
1988 | const is_within_bounds = (mouse_x >= exit_button_extent.x and mouse_y >= exit_button_extent.y and mouse_x <= end_x and mouse_y <= end_y);
1989 | if(is_within_bounds) {
1990 | std.log.info("Close button clicked. Shutdown requested.", .{});
1991 | is_shutdown_requested = true;
1992 | }
1993 | }
1994 | },
1995 | .axis => |axis| {
1996 | std.log.info("Mouse: axis {} {}", .{axis.axis, axis.value.toDouble()});
1997 | },
1998 | .frame => |frame| {
1999 | _ = frame;
2000 | },
2001 | .axis_source => |axis_source| {
2002 | std.log.info("Mouse: axis_source {}", .{axis_source.axis_source});
2003 | },
2004 | .axis_stop => |axis_stop| {
2005 | _ = axis_stop;
2006 | std.log.info("Mouse: axis_stop", .{});
2007 | },
2008 | .axis_discrete => |axis_discrete| {
2009 | _ = axis_discrete;
2010 | std.log.info("Mouse: axis_discrete", .{});
2011 | },
2012 | }
2013 | }
2014 |
2015 | fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, client: *WaylandClient) void {
2016 | switch (event) {
2017 | .global => |global| {
2018 | std.log.info("Wayland: {s}", .{global.interface});
2019 | if (std.mem.orderZ(u8, global.interface, wl.Compositor.getInterface().name) == .eq) {
2020 | client.compositor = registry.bind(global.name, wl.Compositor, 4) catch return;
2021 | } else if (std.mem.orderZ(u8, global.interface, xdg.WmBase.getInterface().name) == .eq) {
2022 | client.xdg_wm_base = registry.bind(global.name, xdg.WmBase, 3) catch return;
2023 | } else if (std.mem.orderZ(u8, global.interface, wl.Seat.getInterface().name) == .eq) {
2024 | client.seat = registry.bind(global.name, wl.Seat, 5) catch return;
2025 | client.pointer = client.seat.getPointer() catch return;
2026 | client.pointer.setListener(*WaylandClient, pointerListener, &wayland_client);
2027 | } else if (std.mem.orderZ(u8, global.interface, wl.Shm.getInterface().name) == .eq) {
2028 | client.cursor_shared_memory = registry.bind(global.name, wl.Shm, 1) catch return;
2029 | } else if (std.mem.orderZ(u8, global.interface, zxdg.DecorationManagerV1.getInterface().name) == .eq) {
2030 | //
2031 | // TODO: Negociate with compositor how the window decorations will be drawn
2032 | //
2033 | draw_window_decorations_requested = false;
2034 | }
2035 | },
2036 | .global_remove => |remove| {
2037 | std.log.info("Wayland global removed: {d}", .{remove.name});
2038 | },
2039 | }
2040 | }
2041 |
2042 | fn waylandSetup() !void {
2043 | wayland_client.display = try wl.Display.connect(null);
2044 | wayland_client.registry = try wayland_client.display.getRegistry();
2045 |
2046 | wayland_client.registry.setListener(*WaylandClient, registryListener, &wayland_client);
2047 |
2048 | if (wayland_client.display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
2049 |
2050 | wayland_client.xdg_wm_base.setListener(*WaylandClient, xdgWmBaseListener, &wayland_client);
2051 |
2052 | wayland_client.surface = try wayland_client.compositor.createSurface();
2053 |
2054 | wayland_client.xdg_surface = try wayland_client.xdg_wm_base.getXdgSurface(wayland_client.surface);
2055 | wayland_client.xdg_surface.setListener(*wl.Surface, xdgSurfaceListener, wayland_client.surface);
2056 |
2057 | wayland_client.xdg_toplevel = try wayland_client.xdg_surface.getToplevel();
2058 | wayland_client.xdg_toplevel.setListener(*bool, xdgToplevelListener, &is_shutdown_requested);
2059 |
2060 | wayland_client.xdg_toplevel.setTitle("vkwayland");
2061 | wayland_client.xdg_toplevel.setAppId("kdchambers.vkwayland");
2062 |
2063 | wayland_client.frame_callback = try wayland_client.surface.frame();
2064 | wayland_client.frame_callback.setListener(*WaylandClient, frameListener, &wayland_client);
2065 |
2066 | wayland_client.cursor_shared_memory.setListener(*WaylandClient, shmListener, &wayland_client);
2067 |
2068 | wayland_client.xdg_toplevel.setTitle(application_name);
2069 | wayland_client.surface.commit();
2070 |
2071 | if (wayland_client.display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
2072 |
2073 |
2074 | //
2075 | // Load cursor theme
2076 | //
2077 |
2078 | wayland_client.cursor_surface = try wayland_client.compositor.createSurface();
2079 |
2080 | const cursor_size = 24;
2081 | wayland_client.cursor_theme = try wl.CursorTheme.load(null, cursor_size, wayland_client.cursor_shared_memory);
2082 | wayland_client.cursor = wayland_client.cursor_theme.getCursor(XCursor.left_ptr).?;
2083 | wayland_client.xcursor = XCursor.left_ptr;
2084 | }
2085 |
2086 | fn waylandDeinit() void
2087 | {
2088 | wayland_client.cursor_surface.destroy();
2089 | wayland_client.cursor_theme.destroy();
2090 |
2091 | wayland_client.cursor_shared_memory.destroy();
2092 |
2093 | wayland_client.pointer.release();
2094 | wayland_client.seat.release();
2095 |
2096 | wayland_client.xdg_wm_base.destroy();
2097 | wayland_client.compositor.destroy();
2098 | wayland_client.registry.destroy();
2099 | _ = wayland_client.display.flush();
2100 | wayland_client.display.disconnect();
2101 | }
2102 |
2103 | //
2104 | // 5. Vulkan Code
2105 | //
2106 |
2107 | fn recreateSwapchain(allocator: std.mem.Allocator, app: *GraphicsContext) !void {
2108 |
2109 | const recreate_swapchain_start = std.time.nanoTimestamp();
2110 |
2111 | _ = try app.device_dispatch.waitForFences(
2112 | app.logical_device,
2113 | 1,
2114 | @ptrCast(&app.inflight_fences[previous_frame]),
2115 | vk.TRUE,
2116 | std.math.maxInt(u64),
2117 | );
2118 |
2119 | for (app.swapchain_image_views) |image_view| {
2120 | app.device_dispatch.destroyImageView(app.logical_device, image_view, null);
2121 | }
2122 |
2123 | app.swapchain_extent.width = screen_dimensions.width;
2124 | app.swapchain_extent.height = screen_dimensions.height;
2125 |
2126 | const old_swapchain = app.swapchain;
2127 | app.swapchain = try app.device_dispatch.createSwapchainKHR(app.logical_device, &vk.SwapchainCreateInfoKHR{
2128 | .surface = app.surface,
2129 | .min_image_count = app.swapchain_min_image_count,
2130 | .image_format = app.surface_format.format,
2131 | .image_color_space = app.surface_format.color_space,
2132 | .image_extent = app.swapchain_extent,
2133 | .image_array_layers = 1,
2134 | .image_usage = .{ .color_attachment_bit = true },
2135 | .image_sharing_mode = .exclusive,
2136 | .queue_family_index_count = 0,
2137 | .p_queue_family_indices = undefined,
2138 | .pre_transform = .{ .identity_bit_khr = true },
2139 | .composite_alpha = alpha_mode,
2140 | .present_mode = .fifo_khr,
2141 | .clipped = vk.TRUE,
2142 | .flags = .{},
2143 | .old_swapchain = old_swapchain,
2144 | }, null);
2145 |
2146 | app.device_dispatch.destroySwapchainKHR(app.logical_device, old_swapchain, null);
2147 |
2148 | var image_count: u32 = undefined;
2149 | {
2150 | if (.success != (try app.device_dispatch.getSwapchainImagesKHR(app.logical_device, app.swapchain, &image_count, null))) {
2151 | return error.FailedToGetSwapchainImagesCount;
2152 | }
2153 |
2154 | if (image_count != app.swapchain_images.len) {
2155 | app.swapchain_images = try allocator.realloc(app.swapchain_images, image_count);
2156 | }
2157 | }
2158 |
2159 | if (.success != (try app.device_dispatch.getSwapchainImagesKHR(app.logical_device, app.swapchain, &image_count, app.swapchain_images.ptr))) {
2160 | return error.FailedToGetSwapchainImages;
2161 | }
2162 | try createSwapchainImageViews(app.*);
2163 |
2164 | for (app.framebuffers) |framebuffer| {
2165 | app.device_dispatch.destroyFramebuffer(app.logical_device, framebuffer, null);
2166 | }
2167 |
2168 | {
2169 | app.framebuffers = try allocator.realloc(app.framebuffers, app.swapchain_image_views.len);
2170 | var framebuffer_create_info = vk.FramebufferCreateInfo{
2171 | .render_pass = app.render_pass,
2172 | .attachment_count = 1,
2173 | // We assign to `p_attachments` below in the loop
2174 | .p_attachments = undefined,
2175 | .width = screen_dimensions.width,
2176 | .height = screen_dimensions.height,
2177 | .layers = 1,
2178 | .flags = .{},
2179 | };
2180 | var i: u32 = 0;
2181 | while (i < app.swapchain_image_views.len) : (i += 1) {
2182 | // We reuse framebuffer_create_info for each framebuffer we create, only we need to update the swapchain_image_view that is attached
2183 | framebuffer_create_info.p_attachments = @ptrCast(&app.swapchain_image_views[i]);
2184 | app.framebuffers[i] = try app.device_dispatch.createFramebuffer(app.logical_device, &framebuffer_create_info, null);
2185 | }
2186 | }
2187 |
2188 | const recreate_swapchain_end = std.time.nanoTimestamp();
2189 | std.debug.assert(recreate_swapchain_end >= recreate_swapchain_start);
2190 | const recreate_swapchain_duration = @as(u64, @intCast(recreate_swapchain_end - recreate_swapchain_start));
2191 |
2192 | std.log.info("Swapchain recreated in {}", .{std.fmt.fmtDuration(recreate_swapchain_duration)});
2193 | }
2194 |
2195 | fn recordRenderPass(
2196 | app: GraphicsContext,
2197 | indices_count: u32,
2198 | ) !void {
2199 | std.debug.assert(app.command_buffers.len > 0);
2200 | std.debug.assert(app.swapchain_images.len == app.command_buffers.len);
2201 | std.debug.assert(screen_dimensions.width == app.swapchain_extent.width);
2202 | std.debug.assert(screen_dimensions.height == app.swapchain_extent.height);
2203 |
2204 | _ = try app.device_dispatch.waitForFences(
2205 | app.logical_device,
2206 | 1,
2207 | @ptrCast(&app.inflight_fences[previous_frame]),
2208 | vk.TRUE,
2209 | std.math.maxInt(u64),
2210 | );
2211 |
2212 | try app.device_dispatch.resetCommandPool(app.logical_device, app.command_pool, .{});
2213 |
2214 | const current_time = std.time.milliTimestamp();
2215 | std.debug.assert(current_time >= background_color_loop_time_base);
2216 |
2217 | // This value should never been seen, as we draw a background quad over it
2218 | const clear_color = graphics.RGBA(f32){
2219 | .r = 0.0,
2220 | .g = 0.0,
2221 | .b = 0.0,
2222 | .a = 1.0 - transparancy_level
2223 | };
2224 | const clear_colors = [1]vk.ClearValue{
2225 | vk.ClearValue{
2226 | .color = vk.ClearColorValue{
2227 | .float_32 = @bitCast(clear_color),
2228 | },
2229 | },
2230 | };
2231 |
2232 | for (app.command_buffers, 0..) |command_buffer, i| {
2233 | try app.device_dispatch.beginCommandBuffer(command_buffer, &vk.CommandBufferBeginInfo{
2234 | .flags = .{},
2235 | .p_inheritance_info = null,
2236 | });
2237 |
2238 | app.device_dispatch.cmdBeginRenderPass(command_buffer, &vk.RenderPassBeginInfo{
2239 | .render_pass = app.render_pass,
2240 | .framebuffer = app.framebuffers[i],
2241 | .render_area = vk.Rect2D{
2242 | .offset = vk.Offset2D{
2243 | .x = 0,
2244 | .y = 0,
2245 | },
2246 | .extent = app.swapchain_extent,
2247 | },
2248 | .clear_value_count = 1,
2249 | .p_clear_values = &clear_colors,
2250 | }, .@"inline");
2251 |
2252 | app.device_dispatch.cmdBindPipeline(command_buffer, .graphics, app.graphics_pipeline);
2253 |
2254 | {
2255 | const viewports = [1]vk.Viewport{
2256 | vk.Viewport{
2257 | .x = 0.0,
2258 | .y = 0.0,
2259 | .width = @floatFromInt(screen_dimensions.width),
2260 | .height = @floatFromInt(screen_dimensions.height),
2261 | .min_depth = 0.0,
2262 | .max_depth = 1.0,
2263 | },
2264 | };
2265 | app.device_dispatch.cmdSetViewport(command_buffer, 0, 1, @ptrCast(&viewports));
2266 | }
2267 | {
2268 | const scissors = [1]vk.Rect2D{
2269 | vk.Rect2D{
2270 | .offset = vk.Offset2D{
2271 | .x = 0,
2272 | .y = 0,
2273 | },
2274 | .extent = vk.Extent2D{
2275 | .width = screen_dimensions.width,
2276 | .height = screen_dimensions.height,
2277 | },
2278 | },
2279 | };
2280 | app.device_dispatch.cmdSetScissor(command_buffer, 0, 1, @ptrCast(&scissors));
2281 | }
2282 |
2283 | app.device_dispatch.cmdBindVertexBuffers(command_buffer, 0, 1, &[1]vk.Buffer{texture_vertices_buffer}, &[1]vk.DeviceSize{0});
2284 | app.device_dispatch.cmdBindIndexBuffer(command_buffer, texture_indices_buffer, 0, .uint16);
2285 | app.device_dispatch.cmdBindDescriptorSets(
2286 | command_buffer,
2287 | .graphics,
2288 | app.pipeline_layout,
2289 | 0,
2290 | 1,
2291 | &[1]vk.DescriptorSet{app.descriptor_sets[i]},
2292 | 0,
2293 | undefined,
2294 | );
2295 |
2296 | const push_constant = PushConstant {
2297 | .width = @floatFromInt(screen_dimensions.width),
2298 | .height = @floatFromInt(screen_dimensions.height),
2299 | .frame = @floatFromInt(frame_count),
2300 | };
2301 |
2302 | app.device_dispatch.cmdPushConstants(
2303 | command_buffer,
2304 | app.pipeline_layout,
2305 | .{ .fragment_bit = true },
2306 | 0,
2307 | @sizeOf(PushConstant),
2308 | &push_constant
2309 | );
2310 |
2311 | app.device_dispatch.cmdDrawIndexed(command_buffer, indices_count, 1, 0, 0, 0);
2312 |
2313 | app.device_dispatch.cmdEndRenderPass(command_buffer);
2314 | try app.device_dispatch.endCommandBuffer(command_buffer);
2315 | }
2316 | }
2317 |
2318 | fn renderFrame(allocator: std.mem.Allocator, app: *GraphicsContext) !void {
2319 | _ = try app.device_dispatch.waitForFences(
2320 | app.logical_device,
2321 | 1,
2322 | @ptrCast(&app.inflight_fences[current_frame]),
2323 | vk.TRUE,
2324 | std.math.maxInt(u64),
2325 | );
2326 |
2327 | const acquire_image_result = try app.device_dispatch.acquireNextImageKHR(
2328 | app.logical_device,
2329 | app.swapchain,
2330 | std.math.maxInt(u64),
2331 | app.images_available[current_frame],
2332 | .null_handle,
2333 | );
2334 |
2335 | // https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/vkAcquireNextImageKHR.html
2336 | switch(acquire_image_result.result) {
2337 | .success => {},
2338 | .error_out_of_date_khr, .suboptimal_khr => {
2339 | try recreateSwapchain(allocator, app);
2340 | return;
2341 | },
2342 | .error_out_of_host_memory => { return error.VulkanHostOutOfMemory; },
2343 | .error_out_of_device_memory => { return error.VulkanDeviceOutOfMemory; },
2344 | .error_device_lost => { return error.VulkanDeviceLost; },
2345 | .error_surface_lost_khr => { return error.VulkanSurfaceLost; },
2346 | .error_full_screen_exclusive_mode_lost_ext => { return error.VulkanFullScreenExclusiveModeLost; },
2347 | .timeout => { return error.VulkanAcquireFramebufferImageTimeout; },
2348 | .not_ready => { return error.VulkanAcquireFramebufferImageNotReady; },
2349 | else => {
2350 | return error.VulkanAcquireNextImageUnknown;
2351 | }
2352 | }
2353 |
2354 | const swapchain_image_index = acquire_image_result.image_index;
2355 |
2356 | const wait_semaphores = [1]vk.Semaphore{app.images_available[current_frame]};
2357 | const wait_stages = [1]vk.PipelineStageFlags{.{ .color_attachment_output_bit = true }};
2358 | const signal_semaphores = [1]vk.Semaphore{app.renders_finished[current_frame]};
2359 |
2360 | const command_submit_info = vk.SubmitInfo{
2361 | .wait_semaphore_count = 1,
2362 | .p_wait_semaphores = &wait_semaphores,
2363 | .p_wait_dst_stage_mask = @ptrCast(@alignCast(&wait_stages)),
2364 | .command_buffer_count = 1,
2365 | .p_command_buffers = @ptrCast(&app.command_buffers[swapchain_image_index]),
2366 | .signal_semaphore_count = 1,
2367 | .p_signal_semaphores = &signal_semaphores,
2368 | };
2369 |
2370 | try app.device_dispatch.resetFences(app.logical_device, 1, @ptrCast(&app.inflight_fences[current_frame]));
2371 | try app.device_dispatch.queueSubmit(
2372 | app.graphics_present_queue,
2373 | 1,
2374 | @ptrCast(&command_submit_info),
2375 | app.inflight_fences[current_frame],
2376 | );
2377 |
2378 | const swapchains = [1]vk.SwapchainKHR{app.swapchain};
2379 | const present_info = vk.PresentInfoKHR{
2380 | .wait_semaphore_count = 1,
2381 | .p_wait_semaphores = &signal_semaphores,
2382 | .swapchain_count = 1,
2383 | .p_swapchains = &swapchains,
2384 | .p_image_indices = @ptrCast(&swapchain_image_index),
2385 | .p_results = null,
2386 | };
2387 |
2388 | const present_result = try app.device_dispatch.queuePresentKHR(app.graphics_present_queue, &present_info);
2389 |
2390 | // https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/vkQueuePresentKHR.html
2391 | switch(present_result) {
2392 | .success => {},
2393 | .error_out_of_date_khr, .suboptimal_khr => {
2394 | try recreateSwapchain(allocator, app);
2395 | return;
2396 | },
2397 | .error_out_of_host_memory => { return error.VulkanHostOutOfMemory; },
2398 | .error_out_of_device_memory => { return error.VulkanDeviceOutOfMemory; },
2399 | .error_device_lost => { return error.VulkanDeviceLost; },
2400 | .error_surface_lost_khr => { return error.VulkanSurfaceLost; },
2401 | .error_full_screen_exclusive_mode_lost_ext => { return error.VulkanFullScreenExclusiveModeLost; },
2402 | .timeout => { return error.VulkanAcquireFramebufferImageTimeout; },
2403 | .not_ready => { return error.VulkanAcquireFramebufferImageNotReady; },
2404 | else => {
2405 | return error.VulkanQueuePresentUnknown;
2406 | }
2407 | }
2408 |
2409 | previous_frame = current_frame;
2410 | current_frame = (current_frame + 1) % max_frames_in_flight;
2411 | }
2412 |
2413 | fn createSwapchainImageViews(app: GraphicsContext) !void {
2414 | for (app.swapchain_image_views, 0..) |*image_view, image_view_i| {
2415 | const image_view_create_info = vk.ImageViewCreateInfo{
2416 | .image = app.swapchain_images[image_view_i],
2417 | .view_type = .@"2d",
2418 | .format = app.surface_format.format,
2419 | .components = vk.ComponentMapping{
2420 | .r = .identity,
2421 | .g = .identity,
2422 | .b = .identity,
2423 | .a = .identity,
2424 | },
2425 | .subresource_range = vk.ImageSubresourceRange{
2426 | .aspect_mask = .{ .color_bit = true },
2427 | .base_mip_level = 0,
2428 | .level_count = 1,
2429 | .base_array_layer = 0,
2430 | .layer_count = 1,
2431 | },
2432 | .flags = .{},
2433 | };
2434 | image_view.* = try app.device_dispatch.createImageView(app.logical_device, &image_view_create_info, null);
2435 | }
2436 | }
2437 |
2438 | fn createRenderPass(app: GraphicsContext) !vk.RenderPass {
2439 | return try app.device_dispatch.createRenderPass(app.logical_device, &vk.RenderPassCreateInfo{
2440 | .attachment_count = 1,
2441 | .p_attachments = &[1]vk.AttachmentDescription{
2442 | .{
2443 | .format = app.surface_format.format,
2444 | .samples = .{ .@"1_bit" = true },
2445 | .load_op = .clear,
2446 | .store_op = .store,
2447 | .stencil_load_op = .dont_care,
2448 | .stencil_store_op = .dont_care,
2449 | .initial_layout = .@"undefined",
2450 | .final_layout = .present_src_khr,
2451 | .flags = .{},
2452 | },
2453 | },
2454 | .subpass_count = 1,
2455 | .p_subpasses = &[1]vk.SubpassDescription{
2456 | .{
2457 | .pipeline_bind_point = .graphics,
2458 | .color_attachment_count = 1,
2459 | .p_color_attachments = &[1]vk.AttachmentReference{
2460 | vk.AttachmentReference{
2461 | .attachment = 0,
2462 | .layout = .color_attachment_optimal,
2463 | },
2464 | },
2465 | .input_attachment_count = 0,
2466 | .p_input_attachments = undefined,
2467 | .p_resolve_attachments = null,
2468 | .p_depth_stencil_attachment = null,
2469 | .preserve_attachment_count = 0,
2470 | .p_preserve_attachments = undefined,
2471 | .flags = .{},
2472 | },
2473 | },
2474 | .dependency_count = 1,
2475 | .p_dependencies = &[1]vk.SubpassDependency{
2476 | .{
2477 | .src_subpass = vk.SUBPASS_EXTERNAL,
2478 | .dst_subpass = 0,
2479 | .src_stage_mask = .{ .color_attachment_output_bit = true },
2480 | .dst_stage_mask = .{ .color_attachment_output_bit = true },
2481 | .src_access_mask = .{},
2482 | .dst_access_mask = .{ .color_attachment_read_bit = true, .color_attachment_write_bit = true },
2483 | .dependency_flags = .{},
2484 | },
2485 | },
2486 | .flags = .{},
2487 | }, null);
2488 | }
2489 |
2490 | fn createDescriptorPool(app: GraphicsContext) !vk.DescriptorPool {
2491 | const image_count: u32 = @intCast(app.swapchain_image_views.len);
2492 | const descriptor_pool_sizes = [_]vk.DescriptorPoolSize{
2493 | .{
2494 | .@"type" = .sampler,
2495 | .descriptor_count = image_count,
2496 | },
2497 | .{
2498 | .@"type" = .sampled_image,
2499 | .descriptor_count = image_count,
2500 | },
2501 | };
2502 | const create_pool_info = vk.DescriptorPoolCreateInfo{
2503 | .pool_size_count = descriptor_pool_sizes.len,
2504 | .p_pool_sizes = &descriptor_pool_sizes,
2505 | .max_sets = image_count,
2506 | .flags = .{},
2507 | };
2508 | return try app.device_dispatch.createDescriptorPool(app.logical_device, &create_pool_info, null);
2509 | }
2510 |
2511 | fn createDescriptorSetLayouts(allocator: std.mem.Allocator, app: GraphicsContext) ![]vk.DescriptorSetLayout {
2512 | var descriptor_set_layouts = try allocator.alloc(vk.DescriptorSetLayout, app.swapchain_image_views.len);
2513 | {
2514 | const descriptor_set_layout_bindings = [_]vk.DescriptorSetLayoutBinding{vk.DescriptorSetLayoutBinding{
2515 | .binding = 0,
2516 | .descriptor_count = 1,
2517 | .descriptor_type = .combined_image_sampler,
2518 | .p_immutable_samplers = null,
2519 | .stage_flags = .{ .fragment_bit = true },
2520 | }};
2521 | const descriptor_set_layout_create_info = vk.DescriptorSetLayoutCreateInfo{
2522 | .binding_count = 1,
2523 | .p_bindings = @ptrCast(&descriptor_set_layout_bindings[0]),
2524 | .flags = .{},
2525 | };
2526 | descriptor_set_layouts[0] = try app.device_dispatch.createDescriptorSetLayout(
2527 | app.logical_device,
2528 | &descriptor_set_layout_create_info,
2529 | null
2530 | );
2531 |
2532 | // We can copy the same descriptor set layout for each swapchain image
2533 | var x: u32 = 1;
2534 | while (x < app.swapchain_image_views.len) : (x += 1) {
2535 | descriptor_set_layouts[x] = descriptor_set_layouts[0];
2536 | }
2537 | }
2538 | return descriptor_set_layouts;
2539 | }
2540 |
2541 | fn createDescriptorSets(
2542 | allocator: std.mem.Allocator,
2543 | app: *GraphicsContext,
2544 | descriptor_set_layouts: []vk.DescriptorSetLayout
2545 | ) ![]vk.DescriptorSet {
2546 | const swapchain_image_count: u32 = @intCast(app.swapchain_image_views.len);
2547 |
2548 | // 1. Allocate DescriptorSets from DescriptorPool
2549 | const descriptor_sets = try allocator.alloc(vk.DescriptorSet, swapchain_image_count);
2550 | {
2551 | const descriptor_set_allocator_info = vk.DescriptorSetAllocateInfo{
2552 | .descriptor_pool = app.descriptor_pool,
2553 | .descriptor_set_count = swapchain_image_count,
2554 | .p_set_layouts = descriptor_set_layouts.ptr,
2555 | };
2556 | try app.device_dispatch.allocateDescriptorSets(
2557 | app.logical_device,
2558 | &descriptor_set_allocator_info,
2559 | @ptrCast(descriptor_sets.ptr)
2560 | );
2561 | }
2562 |
2563 | // 2. Create Sampler that will be written to DescriptorSet
2564 | const sampler_create_info = vk.SamplerCreateInfo{
2565 | .flags = .{},
2566 | .mag_filter = .nearest,
2567 | .min_filter = .nearest,
2568 | .address_mode_u = .clamp_to_edge,
2569 | .address_mode_v = .clamp_to_edge,
2570 | .address_mode_w = .clamp_to_edge,
2571 | .mip_lod_bias = 0.0,
2572 | .anisotropy_enable = vk.FALSE,
2573 | .max_anisotropy = 16.0,
2574 | .border_color = .int_opaque_black,
2575 | .min_lod = 0.0,
2576 | .max_lod = 0.0,
2577 | .unnormalized_coordinates = vk.FALSE,
2578 | .compare_enable = vk.FALSE,
2579 | .compare_op = .always,
2580 | .mipmap_mode = .linear,
2581 | };
2582 | app.sampler = try app.device_dispatch.createSampler(app.logical_device, &sampler_create_info, null);
2583 |
2584 | // 3. Write to DescriptorSets
2585 | var i: u32 = 0;
2586 | while (i < swapchain_image_count) : (i += 1) {
2587 | const descriptor_image_info = [_]vk.DescriptorImageInfo{
2588 | .{
2589 | .image_layout = .shader_read_only_optimal,
2590 | .image_view = texture_image_view,
2591 | .sampler = app.sampler,
2592 | },
2593 | };
2594 | const write_descriptor_set = [_]vk.WriteDescriptorSet{.{
2595 | .dst_set = descriptor_sets[i],
2596 | .dst_binding = 0,
2597 | .dst_array_element = 0,
2598 | .descriptor_type = .combined_image_sampler,
2599 | .descriptor_count = 1,
2600 | .p_image_info = &descriptor_image_info,
2601 | .p_buffer_info = undefined,
2602 | .p_texel_buffer_view = undefined,
2603 | }};
2604 | app.device_dispatch.updateDescriptorSets(app.logical_device, 1, &write_descriptor_set, 0, undefined);
2605 | }
2606 | return descriptor_sets;
2607 | }
2608 |
2609 | fn createPipelineLayout(app: GraphicsContext, descriptor_set_layouts: []vk.DescriptorSetLayout) !vk.PipelineLayout {
2610 | const push_constant = vk.PushConstantRange {
2611 | .stage_flags = .{ .fragment_bit = true },
2612 | .offset = 0,
2613 | .size = @sizeOf(PushConstant),
2614 | };
2615 | const pipeline_layout_create_info = vk.PipelineLayoutCreateInfo{
2616 | .set_layout_count = 1,
2617 | .p_set_layouts = descriptor_set_layouts.ptr,
2618 | .push_constant_range_count = 1,
2619 | .p_push_constant_ranges = @ptrCast(&push_constant),
2620 | .flags = .{},
2621 | };
2622 | return try app.device_dispatch.createPipelineLayout(app.logical_device, &pipeline_layout_create_info, null);
2623 | }
2624 |
2625 | fn createGraphicsPipeline(
2626 | app: GraphicsContext,
2627 | pipeline_layout: vk.PipelineLayout,
2628 | render_pass: vk.RenderPass
2629 | ) !vk.Pipeline {
2630 | const vertex_input_attribute_descriptions = [_]vk.VertexInputAttributeDescription{
2631 | vk.VertexInputAttributeDescription{ // inPosition
2632 | .binding = 0,
2633 | .location = 0,
2634 | .format = .r32g32_sfloat,
2635 | .offset = 0,
2636 | },
2637 | vk.VertexInputAttributeDescription{ // inTexCoord
2638 | .binding = 0,
2639 | .location = 1,
2640 | .format = .r32g32_sfloat,
2641 | .offset = 8,
2642 | },
2643 | vk.VertexInputAttributeDescription{ // inColor
2644 | .binding = 0,
2645 | .location = 2,
2646 | .format = .r32g32b32a32_sfloat,
2647 | .offset = 16,
2648 | },
2649 | };
2650 |
2651 | const vertex_shader_stage_info = vk.PipelineShaderStageCreateInfo{
2652 | .stage = .{ .vertex_bit = true },
2653 | .module = app.vertex_shader_module,
2654 | .p_name = "main",
2655 | .p_specialization_info = null,
2656 | .flags = .{},
2657 | };
2658 |
2659 | const fragment_shader_stage_info = vk.PipelineShaderStageCreateInfo{
2660 | .stage = .{ .fragment_bit = true },
2661 | .module = app.fragment_shader_module,
2662 | .p_name = "main",
2663 | .p_specialization_info = null,
2664 | .flags = .{},
2665 | };
2666 |
2667 | const shader_stages = [2]vk.PipelineShaderStageCreateInfo{
2668 | vertex_shader_stage_info,
2669 | fragment_shader_stage_info,
2670 | };
2671 |
2672 | const vertex_input_binding_descriptions = vk.VertexInputBindingDescription{
2673 | .binding = 0,
2674 | .stride = @sizeOf(graphics.GenericVertex),
2675 | .input_rate = .vertex,
2676 | };
2677 |
2678 | const vertex_input_info = vk.PipelineVertexInputStateCreateInfo{
2679 | .vertex_binding_description_count = @intCast(1),
2680 | .vertex_attribute_description_count = @intCast(3),
2681 | .p_vertex_binding_descriptions = @ptrCast(&vertex_input_binding_descriptions),
2682 | .p_vertex_attribute_descriptions = @ptrCast(&vertex_input_attribute_descriptions),
2683 | .flags = .{},
2684 | };
2685 |
2686 | const input_assembly = vk.PipelineInputAssemblyStateCreateInfo{
2687 | .topology = .triangle_list,
2688 | .primitive_restart_enable = vk.FALSE,
2689 | .flags = .{},
2690 | };
2691 |
2692 | const viewports = [1]vk.Viewport{
2693 | vk.Viewport{
2694 | .x = 0.0,
2695 | .y = 0.0,
2696 | .width = @floatFromInt(screen_dimensions.width),
2697 | .height = @floatFromInt(screen_dimensions.height),
2698 | .min_depth = 0.0,
2699 | .max_depth = 1.0,
2700 | },
2701 | };
2702 |
2703 | const scissors = [1]vk.Rect2D{
2704 | vk.Rect2D{
2705 | .offset = vk.Offset2D{
2706 | .x = 0,
2707 | .y = 0,
2708 | },
2709 | .extent = vk.Extent2D{
2710 | .width = screen_dimensions.width,
2711 | .height = screen_dimensions.height,
2712 | },
2713 | },
2714 | };
2715 |
2716 | const viewport_state = vk.PipelineViewportStateCreateInfo{
2717 | .viewport_count = 1,
2718 | .p_viewports = &viewports,
2719 | .scissor_count = 1,
2720 | .p_scissors = &scissors,
2721 | .flags = .{},
2722 | };
2723 |
2724 | const rasterizer = vk.PipelineRasterizationStateCreateInfo{
2725 | .depth_clamp_enable = vk.FALSE,
2726 | .rasterizer_discard_enable = vk.FALSE,
2727 | .polygon_mode = .fill,
2728 | .line_width = 1.0,
2729 | .cull_mode = .{ .back_bit = true },
2730 | .front_face = .clockwise,
2731 | .depth_bias_enable = vk.FALSE,
2732 | .depth_bias_constant_factor = 0.0,
2733 | .depth_bias_clamp = 0.0,
2734 | .depth_bias_slope_factor = 0.0,
2735 | .flags = .{},
2736 | };
2737 |
2738 | const multisampling = vk.PipelineMultisampleStateCreateInfo{
2739 | .sample_shading_enable = vk.FALSE,
2740 | .rasterization_samples = .{ .@"1_bit" = true },
2741 | .min_sample_shading = 0.0,
2742 | .p_sample_mask = null,
2743 | .alpha_to_coverage_enable = vk.FALSE,
2744 | .alpha_to_one_enable = vk.FALSE,
2745 | .flags = .{},
2746 | };
2747 |
2748 | const color_blend_attachment = vk.PipelineColorBlendAttachmentState{
2749 | .color_write_mask = .{ .r_bit = true, .g_bit = true, .b_bit = true, .a_bit = true },
2750 | .blend_enable = if(enable_blending) vk.TRUE else vk.FALSE,
2751 | .alpha_blend_op = .add,
2752 | .color_blend_op = .add,
2753 | .dst_alpha_blend_factor = .one,
2754 | .src_alpha_blend_factor = .one,
2755 | .dst_color_blend_factor = .one_minus_src_alpha,
2756 | .src_color_blend_factor = .src_alpha,
2757 | };
2758 |
2759 | const blend_constants = [1]f32{0.0} ** 4;
2760 | const color_blending = vk.PipelineColorBlendStateCreateInfo{
2761 | .logic_op_enable = vk.FALSE,
2762 | .logic_op = .copy,
2763 | .attachment_count = 1,
2764 | .p_attachments = @ptrCast(&color_blend_attachment),
2765 | .blend_constants = blend_constants,
2766 | .flags = .{},
2767 | };
2768 |
2769 | const dynamic_states = [_]vk.DynamicState{ .viewport, .scissor };
2770 | const dynamic_state_create_info = vk.PipelineDynamicStateCreateInfo {
2771 | .dynamic_state_count = 2,
2772 | .p_dynamic_states = @ptrCast(&dynamic_states),
2773 | .flags = .{},
2774 | };
2775 |
2776 | const pipeline_create_infos = [1]vk.GraphicsPipelineCreateInfo{
2777 | vk.GraphicsPipelineCreateInfo{
2778 | .stage_count = 2,
2779 | .p_stages = &shader_stages,
2780 | .p_vertex_input_state = &vertex_input_info,
2781 | .p_input_assembly_state = &input_assembly,
2782 | .p_tessellation_state = null,
2783 | .p_viewport_state = &viewport_state,
2784 | .p_rasterization_state = &rasterizer,
2785 | .p_multisample_state = &multisampling,
2786 | .p_depth_stencil_state = null,
2787 | .p_color_blend_state = &color_blending,
2788 | .p_dynamic_state = &dynamic_state_create_info,
2789 | .layout = pipeline_layout,
2790 | .render_pass = render_pass,
2791 | .subpass = 0,
2792 | .base_pipeline_handle = .null_handle,
2793 | .base_pipeline_index = 0,
2794 | .flags = .{},
2795 | },
2796 | };
2797 |
2798 | var graphics_pipeline: vk.Pipeline = undefined;
2799 | _ = try app.device_dispatch.createGraphicsPipelines(
2800 | app.logical_device,
2801 | .null_handle,
2802 | 1,
2803 | &pipeline_create_infos,
2804 | null,
2805 | @ptrCast(&graphics_pipeline)
2806 | );
2807 |
2808 | return graphics_pipeline;
2809 | }
2810 |
2811 | fn createFramebuffers(allocator: std.mem.Allocator, app: GraphicsContext) ![]vk.Framebuffer {
2812 | std.debug.assert(app.swapchain_image_views.len > 0);
2813 | var framebuffer_create_info = vk.FramebufferCreateInfo{
2814 | .render_pass = app.render_pass,
2815 | .attachment_count = 1,
2816 | .p_attachments = undefined,
2817 | .width = screen_dimensions.width,
2818 | .height = screen_dimensions.height,
2819 | .layers = 1,
2820 | .flags = .{},
2821 | };
2822 |
2823 | var framebuffers = try allocator.alloc(vk.Framebuffer, app.swapchain_image_views.len);
2824 | var i: u32 = 0;
2825 | while (i < app.swapchain_image_views.len) : (i += 1) {
2826 | // We reuse framebuffer_create_info for each framebuffer we create,
2827 | // we only need to update the swapchain_image_view that is attached
2828 | framebuffer_create_info.p_attachments = @ptrCast(&app.swapchain_image_views[i]);
2829 | framebuffers[i] = try app.device_dispatch.createFramebuffer(app.logical_device, &framebuffer_create_info, null);
2830 | }
2831 | return framebuffers;
2832 | }
2833 |
2834 | //
2835 | // 6. Vulkan Util / Wrapper Functions
2836 | //
2837 |
2838 | fn createFragmentShaderModule(app: GraphicsContext) !vk.ShaderModule {
2839 | const create_info = vk.ShaderModuleCreateInfo{
2840 | .code_size = shaders.fragment_spv.len,
2841 | .p_code = @ptrCast(@alignCast(shaders.fragment_spv)),
2842 | .flags = .{},
2843 | };
2844 | return try app.device_dispatch.createShaderModule(app.logical_device, &create_info, null);
2845 | }
2846 |
2847 | fn createVertexShaderModule(app: GraphicsContext) !vk.ShaderModule {
2848 | const create_info = vk.ShaderModuleCreateInfo{
2849 | .code_size = shaders.vertex_spv.len,
2850 | .p_code = @ptrCast(@alignCast(shaders.vertex_spv)),
2851 | .flags = .{},
2852 | };
2853 | return try app.device_dispatch.createShaderModule(app.logical_device, &create_info, null);
2854 | }
2855 |
2856 | fn selectSurfaceFormat(
2857 | allocator: std.mem.Allocator,
2858 | app: GraphicsContext,
2859 | color_space: vk.ColorSpaceKHR,
2860 | surface_format: vk.Format,
2861 | ) !?vk.SurfaceFormatKHR {
2862 | var format_count: u32 = undefined;
2863 | if (.success != (try app.instance_dispatch.getPhysicalDeviceSurfaceFormatsKHR(app.physical_device, app.surface, &format_count, null))) {
2864 | return error.FailedToGetSurfaceFormats;
2865 | }
2866 |
2867 | if(format_count == 0) {
2868 | // NOTE: This should not happen. As per spec:
2869 | // "The number of format pairs supported must be greater than or equal to 1"
2870 | // https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/vkGetPhysicalDeviceSurfaceFormatsKHR.html
2871 | std.log.err("Selected surface doesn't support any formats. This may be a vulkan driver bug", .{});
2872 | return error.VulkanSurfaceContainsNoSupportedFormats;
2873 | }
2874 |
2875 | const formats: []vk.SurfaceFormatKHR = try allocator.alloc(vk.SurfaceFormatKHR, format_count);
2876 | defer allocator.free(formats);
2877 |
2878 | if (.success != (try app.instance_dispatch.getPhysicalDeviceSurfaceFormatsKHR(app.physical_device, app.surface, &format_count, formats.ptr))) {
2879 | return error.FailedToGetSurfaceFormats;
2880 | }
2881 |
2882 | for(formats) |format| {
2883 | if(format.format == surface_format and format.color_space == color_space) {
2884 | return format;
2885 | }
2886 | }
2887 | return null;
2888 | }
2889 |
2890 | //
2891 | // 7. Print Functions
2892 | //
2893 |
2894 | fn printVulkanMemoryHeap(memory_properties: vk.PhysicalDeviceMemoryProperties, heap_index: u32, comptime indent_level: u32) void {
2895 |
2896 | const heap_count: u32 = memory_properties.memory_heap_count;
2897 | std.debug.assert(heap_index <= heap_count);
2898 | const base_indent = " " ** indent_level;
2899 |
2900 | const heap_properties = memory_properties.memory_heaps[heap_index];
2901 |
2902 | const print = std.debug.print;
2903 | print(base_indent ++ "Heap Index #{d}\n", .{heap_index});
2904 | print(base_indent ++ " Capacity: {}\n", .{std.fmt.fmtIntSizeDec(heap_properties.size)});
2905 | print(base_indent ++ " Device Local: {}\n", .{heap_properties.flags.device_local_bit});
2906 | print(base_indent ++ " Multi Instance: {}\n", .{heap_properties.flags.multi_instance_bit});
2907 | print(base_indent ++ " Memory Types:\n", .{});
2908 |
2909 | const memory_type_count = memory_properties.memory_type_count;
2910 |
2911 | var memory_type_i: u32 = 0;
2912 | while (memory_type_i < memory_type_count) : (memory_type_i += 1) {
2913 | if(memory_properties.memory_types[memory_type_i].heap_index == heap_index) {
2914 | print(base_indent ++ " Memory Index #{}\n", .{memory_type_i});
2915 | const memory_flags = memory_properties.memory_types[memory_type_i].property_flags;
2916 | print(base_indent ++ " Device Local: {}\n", .{memory_flags.device_local_bit});
2917 | print(base_indent ++ " Host Visible: {}\n", .{memory_flags.host_visible_bit});
2918 | print(base_indent ++ " Host Coherent: {}\n", .{memory_flags.host_coherent_bit});
2919 | print(base_indent ++ " Host Cached: {}\n", .{memory_flags.host_cached_bit});
2920 | print(base_indent ++ " Lazily Allocated: {}\n", .{memory_flags.lazily_allocated_bit});
2921 | print(base_indent ++ " Protected: {}\n", .{memory_flags.protected_bit});
2922 | }
2923 | }
2924 | }
2925 |
2926 | fn printVulkanMemoryHeaps(memory_properties: vk.PhysicalDeviceMemoryProperties, comptime indent_level: u32) void {
2927 | const heap_count: u32 = memory_properties.memory_heap_count;
2928 | var heap_i: u32 = 0;
2929 | while (heap_i < heap_count) : (heap_i += 1) {
2930 | printVulkanMemoryHeap(memory_properties, heap_i, indent_level);
2931 | }
2932 | }
2933 |
2934 | fn printVulkanQueueFamilies(queue_families: []vk.QueueFamilyProperties, comptime indent_level: u32) void {
2935 | const print = std.debug.print;
2936 | const base_indent = " " ** indent_level;
2937 | for(queue_families, 0..) |queue_family, queue_family_i| {
2938 | print(base_indent ++ "Queue family index #{d}\n", .{queue_family_i});
2939 | printVulkanQueueFamily(queue_family, indent_level + 1);
2940 | }
2941 | }
2942 |
2943 | fn printVulkanQueueFamily(queue_family: vk.QueueFamilyProperties, comptime indent_level: u32) void {
2944 | const print = std.debug.print;
2945 | const base_indent = " " ** indent_level;
2946 | print(base_indent ++ "Queue count: {d}\n", .{queue_family.queue_count});
2947 | print(base_indent ++ "Support\n", .{});
2948 | print(base_indent ++ " Graphics: {}\n", .{queue_family.queue_flags.graphics_bit});
2949 | print(base_indent ++ " Transfer: {}\n", .{queue_family.queue_flags.transfer_bit});
2950 | print(base_indent ++ " Compute: {}\n", .{queue_family.queue_flags.compute_bit});
2951 | }
2952 |
2953 | fn printSurfaceCapabilities(surface_capabilities: vk.SurfaceCapabilitiesKHR, comptime indent_level: u32) void {
2954 | const print = std.debug.print;
2955 | const base_indent = " " ** indent_level;
2956 | print(base_indent ++ "min_image_count: {d}\n", .{surface_capabilities.min_image_count});
2957 | print(base_indent ++ "max_image_count: {d}\n", .{surface_capabilities.max_image_count});
2958 |
2959 | print(base_indent ++ "current_extent\n", .{});
2960 | print(base_indent ++ " width: {d}\n", .{surface_capabilities.current_extent.width});
2961 | print(base_indent ++ " height: {d}\n", .{surface_capabilities.current_extent.height});
2962 |
2963 | print(base_indent ++ "min_image_extent\n", .{});
2964 | print(base_indent ++ " width: {d}\n", .{surface_capabilities.min_image_extent.width});
2965 | print(base_indent ++ " height: {d}\n", .{surface_capabilities.min_image_extent.height});
2966 |
2967 | print(base_indent ++ "max_image_extent\n", .{});
2968 | print(base_indent ++ " width: {d}\n", .{surface_capabilities.max_image_extent.width});
2969 | print(base_indent ++ " height: {d}\n", .{surface_capabilities.max_image_extent.height});
2970 | print(base_indent ++ "max_image_array_layers: {d}\n", .{surface_capabilities.max_image_array_layers});
2971 |
2972 | print(base_indent ++ "supported_usages\n", .{});
2973 | const supported_usage_flags = surface_capabilities.supported_usage_flags;
2974 | print(base_indent ++ " sampled: {}\n", .{supported_usage_flags.sampled_bit});
2975 | print(base_indent ++ " storage: {}\n", .{supported_usage_flags.storage_bit});
2976 | print(base_indent ++ " color_attachment: {}\n", .{supported_usage_flags.color_attachment_bit});
2977 | print(base_indent ++ " depth_stencil: {}\n", .{supported_usage_flags.depth_stencil_attachment_bit});
2978 | print(base_indent ++ " input_attachment: {}\n", .{supported_usage_flags.input_attachment_bit});
2979 | print(base_indent ++ " transient_attachment: {}\n", .{supported_usage_flags.transient_attachment_bit});
2980 | print(base_indent ++ " fragment_shading_rate_attachment: {}\n", .{supported_usage_flags.fragment_shading_rate_attachment_bit_khr});
2981 | print(base_indent ++ " fragment_density_map: {}\n", .{supported_usage_flags.fragment_density_map_bit_ext});
2982 | print(base_indent ++ " video_decode_dst: {}\n", .{supported_usage_flags.video_decode_dst_bit_khr});
2983 | print(base_indent ++ " video_decode_dpb: {}\n", .{supported_usage_flags.video_decode_dpb_bit_khr});
2984 | print(base_indent ++ " video_encode_src: {}\n", .{supported_usage_flags.video_encode_src_bit_khr});
2985 | print(base_indent ++ " video_encode_dpb: {}\n", .{supported_usage_flags.video_encode_dpb_bit_khr});
2986 |
2987 | print(base_indent ++ "supportedCompositeAlpha:\n", .{});
2988 | print(base_indent ++ " Opaque KHR {}\n", .{surface_capabilities.supported_composite_alpha.opaque_bit_khr});
2989 | print(base_indent ++ " Pre Mult KHR {}\n", .{surface_capabilities.supported_composite_alpha.pre_multiplied_bit_khr});
2990 | print(base_indent ++ " Post Mult KHR {}\n", .{surface_capabilities.supported_composite_alpha.post_multiplied_bit_khr});
2991 | print(base_indent ++ " Inherit KHR {}\n", .{surface_capabilities.supported_composite_alpha.inherit_bit_khr});
2992 | }
2993 |
2994 | //
2995 | // 8. Util + Misc
2996 | //
2997 |
2998 | fn lerp(from: f32, to: f32, value: f32) f32 {
2999 | return from + (value * (to - from));
3000 | }
3001 |
3002 |
--------------------------------------------------------------------------------
/src/vulkan_config.zig:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | // Copyright (c) 2022 Keith Chambers
3 |
4 | const std = @import("std");
5 | const vk = @import("vulkan");
6 |
7 | pub const BaseDispatch = vk.BaseWrapper(.{
8 | .createInstance = true,
9 | });
10 |
11 | pub const InstanceDispatch = vk.InstanceWrapper(.{
12 | .destroyInstance = true,
13 | .createDevice = true,
14 | .destroySurfaceKHR = true,
15 | .enumeratePhysicalDevices = true,
16 | .getPhysicalDeviceProperties = true,
17 | .enumerateDeviceExtensionProperties = true,
18 | .getPhysicalDeviceSurfaceFormatsKHR = true,
19 | .getPhysicalDeviceSurfacePresentModesKHR = true,
20 | .getPhysicalDeviceSurfaceCapabilitiesKHR = true,
21 | .getPhysicalDeviceQueueFamilyProperties = true,
22 | .getPhysicalDeviceSurfaceSupportKHR = true,
23 | .getPhysicalDeviceMemoryProperties = true,
24 | .getDeviceProcAddr = true,
25 | .createWaylandSurfaceKHR = true,
26 | });
27 |
28 | pub const DeviceDispatch = vk.DeviceWrapper(.{
29 | .destroyDevice = true,
30 | .getDeviceQueue = true,
31 | .createSemaphore = true,
32 | .createFence = true,
33 | .createImage = true,
34 | .destroyImage = true,
35 | .createImageView = true,
36 | .destroyImageView = true,
37 | .destroySemaphore = true,
38 | .destroyFence = true,
39 | .getSwapchainImagesKHR = true,
40 | .createSwapchainKHR = true,
41 | .destroySwapchainKHR = true,
42 | .acquireNextImageKHR = true,
43 | .deviceWaitIdle = true,
44 | .waitForFences = true,
45 | .resetFences = true,
46 | .queueSubmit = true,
47 | .queuePresentKHR = true,
48 | .createCommandPool = true,
49 | .destroyCommandPool = true,
50 | .allocateCommandBuffers = true,
51 | .freeCommandBuffers = true,
52 | .queueWaitIdle = true,
53 | .createShaderModule = true,
54 | .destroyShaderModule = true,
55 | .createPipelineLayout = true,
56 | .destroyPipelineLayout = true,
57 | .createRenderPass = true,
58 | .destroyRenderPass = true,
59 | .createGraphicsPipelines = true,
60 | .destroyPipeline = true,
61 | .createFramebuffer = true,
62 | .destroyFramebuffer = true,
63 | .beginCommandBuffer = true,
64 | .endCommandBuffer = true,
65 | .allocateMemory = true,
66 | .freeMemory = true,
67 | .createBuffer = true,
68 | .destroyBuffer = true,
69 | .getBufferMemoryRequirements = true,
70 | .mapMemory = true,
71 | .unmapMemory = true,
72 | .bindBufferMemory = true,
73 | .cmdBeginRenderPass = true,
74 | .cmdEndRenderPass = true,
75 | .cmdBindPipeline = true,
76 | .cmdDraw = true,
77 | .cmdSetViewport = true,
78 | .cmdSetScissor = true,
79 | .cmdBindVertexBuffers = true,
80 | .cmdCopyBuffer = true,
81 | .cmdDrawIndexed = true,
82 | .getImageMemoryRequirements = true,
83 | .bindImageMemory = true,
84 | .cmdPipelineBarrier = true,
85 | .createDescriptorSetLayout = true,
86 | .destroyDescriptorSetLayout = true,
87 | .createDescriptorPool = true,
88 | .destroyDescriptorPool = true,
89 | .allocateDescriptorSets = true,
90 | .freeDescriptorSets = true,
91 | .createSampler = true,
92 | .destroySampler = true,
93 | .updateDescriptorSets = true,
94 | .resetCommandPool = true,
95 | .cmdBindIndexBuffer = true,
96 | .cmdBindDescriptorSets = true,
97 | .cmdPushConstants = true,
98 | .cmdCopyBufferToImage = true,
99 | });
100 |
101 | /// The features that we request to be enabled on our selected physical device
102 | pub const enabled_device_features = vk.PhysicalDeviceFeatures{
103 | .sampler_anisotropy = vk.TRUE,
104 | // The rest are set to false
105 | // https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VkPhysicalDeviceFeatures.html
106 | .robust_buffer_access = vk.FALSE,
107 | .full_draw_index_uint_32 = vk.FALSE,
108 | .image_cube_array = vk.FALSE,
109 | .independent_blend = vk.FALSE,
110 | .geometry_shader = vk.FALSE,
111 | .tessellation_shader = vk.FALSE,
112 | .sample_rate_shading = vk.FALSE,
113 | .dual_src_blend = vk.FALSE,
114 | .logic_op = vk.FALSE,
115 | .multi_draw_indirect = vk.FALSE,
116 | .draw_indirect_first_instance = vk.FALSE,
117 | .depth_clamp = vk.FALSE,
118 | .depth_bias_clamp = vk.FALSE,
119 | .fill_mode_non_solid = vk.FALSE,
120 | .depth_bounds = vk.FALSE,
121 | .wide_lines = vk.FALSE,
122 | .large_points = vk.FALSE,
123 | .alpha_to_one = vk.FALSE,
124 | .multi_viewport = vk.FALSE,
125 | .texture_compression_etc2 = vk.FALSE,
126 | .texture_compression_astc_ldr = vk.FALSE,
127 | .texture_compression_bc = vk.FALSE,
128 | .occlusion_query_precise = vk.FALSE,
129 | .pipeline_statistics_query = vk.FALSE,
130 | .vertex_pipeline_stores_and_atomics = vk.FALSE,
131 | .fragment_stores_and_atomics = vk.FALSE,
132 | .shader_tessellation_and_geometry_point_size = vk.FALSE,
133 | .shader_image_gather_extended = vk.FALSE,
134 | .shader_storage_image_extended_formats = vk.FALSE,
135 | .shader_storage_image_multisample = vk.FALSE,
136 | .shader_storage_image_read_without_format = vk.FALSE,
137 | .shader_storage_image_write_without_format = vk.FALSE,
138 | .shader_uniform_buffer_array_dynamic_indexing = vk.FALSE,
139 | .shader_sampled_image_array_dynamic_indexing = vk.FALSE,
140 | .shader_storage_buffer_array_dynamic_indexing = vk.FALSE,
141 | .shader_storage_image_array_dynamic_indexing = vk.FALSE,
142 | .shader_clip_distance = vk.FALSE,
143 | .shader_cull_distance = vk.FALSE,
144 | .shader_float_64 = vk.FALSE,
145 | .shader_int_64 = vk.FALSE,
146 | .shader_int_16 = vk.FALSE,
147 | .shader_resource_residency = vk.FALSE,
148 | .shader_resource_min_lod = vk.FALSE,
149 | .sparse_binding = vk.FALSE,
150 | .sparse_residency_buffer = vk.FALSE,
151 | .sparse_residency_image_2d = vk.FALSE,
152 | .sparse_residency_image_3d = vk.FALSE,
153 | .sparse_residency_2_samples = vk.FALSE,
154 | .sparse_residency_4_samples = vk.FALSE,
155 | .sparse_residency_8_samples = vk.FALSE,
156 | .sparse_residency_16_samples = vk.FALSE,
157 | .sparse_residency_aliased = vk.FALSE,
158 | .variable_multisample_rate = vk.FALSE,
159 | .inherited_queries = vk.FALSE,
160 | };
161 |
--------------------------------------------------------------------------------