diff --git a/.gitignore b/.gitignore index 02689f6..880cd5d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -/zig-out -/.zig-cache +zig-out +.zig-cache diff --git a/.ignore b/.ignore new file mode 100644 index 0000000..2fd4c3b --- /dev/null +++ b/.ignore @@ -0,0 +1 @@ +libs/ diff --git a/build.zig b/build.zig index 7796f98..57b16ac 100644 --- a/build.zig +++ b/build.zig @@ -2,19 +2,9 @@ const std = @import("std"); const sdl = @import("sdl"); const vkgen = @import("vulkan_zig"); -// Although this function looks imperative, note that its job is to -// declaratively construct a build graph that will be executed by an external -// runner. pub fn build(b: *std.Build) void { - // Standard target options allows the person running `zig build` to choose - // what target to build for. Here we do not override the defaults, which - // means any target is allowed, and the default is native. Other options - // for restricting supported target set are available. const target = b.standardTargetOptions(.{}); - // Standard optimization options allow the person running `zig build` to select - // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not - // set a preferred release mode, allowing the user to decide how to optimize. const optimize = b.standardOptimizeOption(.{}); const exe = b.addExecutable(.{ @@ -24,6 +14,7 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }); + // Vulkan const vkzig_dep = b.dependency("vulkan_zig", .{ .registry = @as([]const u8, b.pathFromRoot("./vk.xml")), }); @@ -39,35 +30,24 @@ pub fn build(b: *std.Build) void { shader_comp.add("shader_vert", "src/shaders/shader.vert", .{}); exe.root_module.addImport("shaders", shader_comp.getModule()); + // SDL2 const sdl_sdk = sdl.init(b, null); sdl_sdk.link(exe, .dynamic); - exe.root_module.addImport("sdl2", sdl_sdk.getWrapperModuleVulkan(vkzig_bindings)); - // This declares intent for the executable to be installed into the - // standard location when the user invokes the "install" step (the default - // step when running `zig build`). + b.installArtifact(exe); - // This *creates* a Run step in the build graph, to be executed when another - // step is evaluated that depends on it. The next line below will establish - // such a dependency. + const check = b.step("check", "Check if vulkan-test compiles"); + check.dependOn(&exe.step); + const run_cmd = b.addRunArtifact(exe); - // By making the run step depend on the install step, it will be run from the - // installation directory rather than directly from within the cache directory. - // This is not necessary, however, if the application depends on other installed - // files, this ensures they will be present and in the expected location. run_cmd.step.dependOn(b.getInstallStep()); - // This allows the user to pass arguments to the application in the build - // command itself, like this: `zig build run -- arg1 arg2 etc` if (b.args) |args| { run_cmd.addArgs(args); } - // This creates a build step. It will be visible in the `zig build --help` menu, - // and can be selected like this: `zig build run` - // This will evaluate the `run` step rather than the default, which is "install". const run_step = b.step("run", "Run the app"); run_step.dependOn(&run_cmd.step); @@ -79,9 +59,6 @@ pub fn build(b: *std.Build) void { const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); - // Similar to creating the run step earlier, this exposes a `test` step to - // the `zig build --help` menu, providing a way for the user to request - // running the unit tests. const test_step = b.step("test", "Run unit tests"); test_step.dependOn(&run_exe_unit_tests.step); } diff --git a/build.zig.zon b/build.zig.zon index df76479..db63e70 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,27 +1,8 @@ .{ - // This is the default name used by packages depending on this one. For - // example, when a user runs `zig fetch --save `, this field is used - // as the key in the `dependencies` table. Although the user can choose a - // different name, most users will stick with this provided value. - // - // It is redundant to include "zig" in this name because it is already - // within the Zig package namespace. .name = "vulkan-test", - // This is a [Semantic Version](https://semver.org/). - // In a future version of Zig it will be used for package deduplication. - .version = "0.0.0", + .version = "0.1.0", - // This field is optional. - // This is currently advisory only; Zig does not yet do anything - // with this value. - //.minimum_zig_version = "0.11.0", - - // This field is optional. - // Each dependency must either provide a `url` and `hash`, or a `path`. - // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. - // Once all dependencies are fetched, `zig build` no longer requires - // internet connectivity. .dependencies = .{ .vulkan_zig = .{ .url = "https://github.com/Snektron/vulkan-zig/archive/f637a0d2525f62c7803c18c89a95cc2f8d8b2789.tar.gz", @@ -32,6 +13,7 @@ .hash = "1220821a34cc5fa538f4f5f96541322019e73104586e4f3fbafc614646e8e9bf64d0", }, }, + .paths = .{ "build.zig", "build.zig.zon", diff --git a/src/Mesh.zig b/src/Mesh.zig new file mode 100644 index 0000000..fb0bdcf --- /dev/null +++ b/src/Mesh.zig @@ -0,0 +1,94 @@ +const std = @import("std"); +const vk = @import("vulkan"); +const Utilities = @import("utilities.zig"); +const Vertex = Utilities.Vertex; +const Device = @import("vulkan_renderer.zig").Device; +const Instance = @import("vulkan_renderer.zig").Instance; + +const Self = @This(); + +vertex_count: u32, +vertex_buffer: vk.Buffer, +vertex_buffer_memory: vk.DeviceMemory, + +instance: Instance, +physical_device: vk.PhysicalDevice, +device: Device, + +pub fn new(instance: Instance, pdev: vk.PhysicalDevice, device: Device, vertices: []const Vertex) !Self { + var mesh: Self = undefined; + + mesh.vertex_count = @intCast(vertices.len); + mesh.instance = instance; + mesh.physical_device = pdev; + mesh.device = device; + try mesh.createVertexBuffer(vertices); + + return mesh; +} + +pub fn destroyVertexBuffer(self: Self) void { + self.device.destroyBuffer(self.vertex_buffer, null); + self.device.freeMemory(self.vertex_buffer_memory, null); +} + +fn createVertexBuffer(self: *Self, vertices: []const Vertex) !void { + // Create vertex buffer + // Information to create buffer (doesn't include assigning memory) + const buffer_create_info: vk.BufferCreateInfo = .{ + .size = @sizeOf(Vertex) * vertices.len, // Size of buffer (size of 1 vertex * number of vertices) + .usage = .{ .vertex_buffer_bit = true }, // Multiple types of buffer possible, we want vertex buffer + .sharing_mode = .exclusive, // Similar to swapchain images, can share vertex buffers + }; + + self.vertex_buffer = try self.device.createBuffer(&buffer_create_info, null); + + // Get buffer memory requirements + const mem_requirements = self.device.getBufferMemoryRequirements(self.vertex_buffer); + + // Allocate memory to buffer + const allocate_info: vk.MemoryAllocateInfo = .{ + .allocation_size = mem_requirements.size, + .memory_type_index = self.findMemoryTypeIndex( + mem_requirements.memory_type_bits, // Index of memory type of physical device that has required bit flags + .{ + .host_visible_bit = true, // CPU can interact with memory + .host_coherent_bit = true, // Allows placement of data straight into buffer after mapping (otherwise would have to specify manually) + }, + ), + }; + + // Allocate memory to vkDeviceMemory + self.vertex_buffer_memory = try self.device.allocateMemory(&allocate_info, null); + + // Allocate memory to given vertex buffer + try self.device.bindBufferMemory(self.vertex_buffer, self.vertex_buffer_memory, 0); + + // Map memory to vertex + // 1. Create pointer to a point in normal memory + // 2. Map the vertex buffer memory to that point + const data = try self.device.mapMemory(self.vertex_buffer_memory, 0, vk.WHOLE_SIZE, .{}); + + // 3. Copy memory from vertices vector to the point in memory + const gpu_vertices: [*]Vertex = @ptrCast(@alignCast(data)); + @memcpy(gpu_vertices, vertices[0..]); + + // 4. Unmap the vertex buffer memory + self.device.unmapMemory(self.vertex_buffer_memory); +} + +fn findMemoryTypeIndex(self: Self, allowed_types: u32, properties: vk.MemoryPropertyFlags) u32 { + // Get properties of physical device memory + const memory_properties = self.instance.getPhysicalDeviceMemoryProperties(self.physical_device); + const mem_type_count = memory_properties.memory_type_count; + + for (memory_properties.memory_types[0..mem_type_count], 0..mem_type_count) |mem_type, i| { + // Index of memory type must match corresponding bit in allowed_types + if (allowed_types & (@as(u32, 1) << @truncate(i)) != 0 and mem_type.property_flags.contains(properties)) { + // Return the index of the valid memory type + return @truncate(i); + } + } + + unreachable; +} diff --git a/src/shaders/shader.frag b/src/shaders/shader.frag index edeac14..40de65c 100644 --- a/src/shaders/shader.frag +++ b/src/shaders/shader.frag @@ -1,11 +1,8 @@ #version 450 -// Interpolated colour from vertex (location must match) -layout(location = 0) in vec3 fragColour; - // Final output output (must also have location) layout(location = 0) out vec4 outColour; void main() { - outColour = vec4(fragColour, 1.0); + outColour = vec4(1.0, 0.0, 0.0, 1.0); } diff --git a/src/shaders/shader.vert b/src/shaders/shader.vert index 7a51229..340b1bc 100644 --- a/src/shaders/shader.vert +++ b/src/shaders/shader.vert @@ -1,23 +1,8 @@ #version 450 -// Output colour for vertex (location is required) -layout(location = 0) out vec3 fragColour; - -// Triangle vertex positions -vec3 positions[3] = vec3[]( - vec3(0.0, -0.4, 0.0), - vec3(0.4, 0.4, 0.0), - vec3(-0.4, 0.4, 0.0) - ); - -// Triangle vertex colours -vec3 colours[3] = vec3[]( - vec3(1.0, 0.0, 0.0), - vec3(0.0, 1.0, 0.0), - vec3(0.0, 0.0, 1.0) - ); +layout(location = 0) in vec3 pos; void main() { - gl_Position = vec4(positions[gl_VertexIndex], 1.0); - fragColour = colours[gl_VertexIndex]; + gl_Position = vec4(pos, 1.0); + // fragColour = colours[gl_VertexIndex]; } diff --git a/src/utilities.zig b/src/utilities.zig index ae96b47..fa766d5 100644 --- a/src/utilities.zig +++ b/src/utilities.zig @@ -3,6 +3,12 @@ const vk = @import("vulkan"); pub const device_extensions = [_][*:0]const u8{vk.extensions.khr_swapchain.name}; +// Vertex data representation +pub const Vertex = struct { + // Vertex position (x, y, z) + pos: @Vector(3, f32), +}; + pub const QueueFamilyIndices = struct { graphics_family: ?u32 = null, presentation_family: ?u32 = null, diff --git a/src/vulkan_renderer.zig b/src/vulkan_renderer.zig index 149ce94..bf41aa8 100644 --- a/src/vulkan_renderer.zig +++ b/src/vulkan_renderer.zig @@ -8,6 +8,9 @@ const Utilities = @import("utilities.zig"); const QueueFamilyIndices = Utilities.QueueFamilyIndices; const SwapchainDetails = Utilities.SwapchainDetails; const SwapchainImage = Utilities.SwapchainImage; +const Vertex = Utilities.Vertex; + +const Mesh = @import("Mesh.zig"); const enable_validation_layers = builtin.mode == .Debug; const validation_layers = [_][*:0]const u8{"VK_LAYER_KHRONOS_validation"}; @@ -28,10 +31,10 @@ const BaseDispatch = vk.BaseWrapper(apis); const InstanceDispatch = vk.InstanceWrapper(apis); const DeviceDispatch = vk.DeviceWrapper(apis); -const Instance = vk.InstanceProxy(apis); -const Device = vk.DeviceProxy(apis); -const Queue = vk.QueueProxy(apis); -const CommandBuffer = vk.CommandBufferProxy(apis); +pub const Instance = vk.InstanceProxy(apis); +pub const Device = vk.DeviceProxy(apis); +pub const Queue = vk.QueueProxy(apis); +pub const CommandBuffer = vk.CommandBufferProxy(apis); pub const VulkanRenderer = struct { const Self = @This(); @@ -44,6 +47,9 @@ pub const VulkanRenderer = struct { current_frame: u32 = 0, + // Scene objects + first_mesh: Mesh, + // Main instance: Instance, physical_device: vk.PhysicalDevice, @@ -95,6 +101,16 @@ pub const VulkanRenderer = struct { try self.getPhysicalDevice(); try self.createLogicalDevice(); + + // Create mesh + var mesh_vertices = [_]Vertex{ + .{ .pos = .{ 0.0, -0.4, 0.0 } }, + .{ .pos = .{ 0.4, 0.4, 0.0 } }, + .{ .pos = .{ -0.4, 0.4, 0.0 } }, + }; + + self.first_mesh = try Mesh.new(self.instance, self.physical_device, self.device, &mesh_vertices); + try self.createSwapchain(); try self.createRenderPass(); try self.createGraphicsPipeline(); @@ -157,11 +173,13 @@ pub const VulkanRenderer = struct { } pub fn deinit(self: *Self) void { + self.device.deviceWaitIdle() catch undefined; + if (enable_validation_layers) { self.instance.destroyDebugUtilsMessengerEXT(self.debug_utils.?, null); } - self.device.deviceWaitIdle() catch undefined; + self.first_mesh.destroyVertexBuffer(); for (0..MAX_FRAME_DRAWS) |i| { self.device.destroySemaphore(self.render_finished[i], null); @@ -470,10 +488,32 @@ pub const VulkanRenderer = struct { fragment_shader_create_info, }; - // -- Vertex input (TODO: Put in vertex descriptions when resources created) -- + // How the data for a single vertex (including info such as position, colour, texture coords, normals, etc...) is as a whole + const binding_description: vk.VertexInputBindingDescription = .{ + .binding = 0, // Can bind multiple streams of data, this defines which one + .stride = @sizeOf(Vertex), // Size of simple vertex object + .input_rate = .vertex, // How to move between data after each vertex + // vertex: move to the next vertex + // instance: move to a vertex for the next instance + }; + + // How the data for an attribute is defined within the vertex + const attribute_descriptions = [_]vk.VertexInputAttributeDescription{ + // Position attribute + .{ + .binding = 0, // Which binding the data is at (should be same as above) + .location = 0, // Location in shader where data will be read from + .format = vk.Format.r32g32b32_sfloat, // Format the data will take (also helps define size of data) + .offset = @offsetOf(Vertex, "pos"), // Where this attribute is defined in data for a single vertex + }, + }; + + // -- Vertex input -- const vertex_input_create_info: vk.PipelineVertexInputStateCreateInfo = .{ - .p_vertex_binding_descriptions = null, // List of vertex binding descriptions (data spacing, stride info) - .p_vertex_attribute_descriptions = null, // List of vertex attribute descriptions (data format and where to bind to/from) + .vertex_binding_description_count = 1, + .p_vertex_binding_descriptions = @ptrCast(&binding_description), // List of vertex binding descriptions (data spacing, stride info) + .vertex_attribute_description_count = @intCast(attribute_descriptions.len), + .p_vertex_attribute_descriptions = &attribute_descriptions, // List of vertex attribute descriptions (data format and where to bind to/from) }; // -- Input assembly -- @@ -697,15 +737,24 @@ pub const VulkanRenderer = struct { // Begin render pass command_buffer.beginRenderPass(&render_pass_begin_info, vk.SubpassContents.@"inline"); - // Bind pipeline to be used in render pass - command_buffer.bindPipeline(.graphics, self.graphics_pipeline); + { + // Bind pipeline to be used in render pass + command_buffer.bindPipeline(.graphics, self.graphics_pipeline); - // Needed when using dynamic state - command_buffer.setViewport(0, 1, @ptrCast(&self.viewport)); - command_buffer.setScissor(0, 1, @ptrCast(&self.scissor)); + // Buffers to bind + const vertex_buffers = [_]vk.Buffer{self.first_mesh.vertex_buffer}; + // Offsets into buffers being bound + const offsets = [_]vk.DeviceSize{0}; + // Command to bind vertex buffer before drawing with them + command_buffer.bindVertexBuffers(0, 1, &vertex_buffers, &offsets); - // Execute a pipeline - command_buffer.draw(3, 1, 0, 0); + // Needed when using dynamic state + command_buffer.setViewport(0, 1, @ptrCast(&self.viewport)); + command_buffer.setScissor(0, 1, @ptrCast(&self.scissor)); + + // Execute a pipeline + command_buffer.draw(self.first_mesh.vertex_count, 1, 0, 0); + } // End render pass command_buffer.endRenderPass();