├── .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 | ![Screenshot](assets/screenshot_fedora_gnome.png) 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 | --------------------------------------------------------------------------------