Vala プログラミング

WebGPU プログラミング

おなが@京都先端科学大

Swift Vulkan & GNUStepSwiftBridge

Swif言語を用いてVulkanによる描画にトライしました。
GNUStepSwiftBridgeと組合せて、NSWindow上のボタンから実行できるように
しています。Vulkanの描画は、glfwを使ってXWindowに描画します。

[ 実行結果 ]
Triangle

UniformBuffer

ネット検索すると、Swift言語で書かれたVulkanのライブラリやサンプルはいくつか
見つかりますが、エラー無しに実行できるものは少ししかありません。
その中で、
  henrybetts/swift-vulkan: A natural Swift interface for Vulkan
  https://github.com/henrybetts/swift-vulkan
を参考にしました。

[ サンプルプログラムの実行結果 ]
DemoClearColor

(/swift-vulkan/Demos/swift-vulkan-demosディレクトリで
 swift run DemoClearColorを実行)

DemoTriangle

(shaderのパスを "./Sources/DemoTriangle/shaders/vert.spv" のように変更)
glfwを使って、XWindowに表示しています。

今回は、このサンプルプログラムとGNUStepSwiftBridgeを組合せました。
また、UniformBufferによる描画を追加しました。

ディレクトリ配置

AppVulkan
   Package.swift
   /AppKit
   /Sources
       /CGLFW
       /CVulkan
       /Vulkan
       /SGLMath
       /Main
           /Resources
           /shaders
           /shaders_ubo
           CheckResource.swift
           main.swift
           AppDelegate.swift
           AppError.swift
           AppClearColor.swift
           AppTriangle.swift
           AppUniformBuffer.swift
           color.swift
           file.swift

AppKitの配置を変更しています。内容は前回同様です。

AppKit
   Package.swift
   /Sources
      /Headers
      /AppKit
      /AppKitGNUStep
      /FoubdationGNUStep
      /libobjc2
      /ObjCSwiftInterop

Package.swift
   // swift-tools-version: 5.8
   // The swift-tools-version declares the minimum version of Swift required to build this package.

   import PackageDescription

   let package = Package(
      name: "AppKit",
      products: [
         // Products define the executables and libraries a package produces, making them visible to other packages.
         .library(name: "libobjc", targets: ["libobjc2"]),
         .library(name: "AppKitGNUStep", targets: ["AppKitGNUStep"]),
	 .library(name: "FoundationGNUStep", targets: ["FoundationGNUStep"]),
	 .library(name: "ObjCSwiftInterop",  targets: ["ObjCSwiftInterop"]),
         .library(name: "AppKit", targets: ["AppKit"]),
      ],
      targets: [
         // Targets are the basic building blocks of a package, defining a module or a test suite.
         // Targets can depend on other targets in this package and products from dependencies.
         .target(name: "ObjCSwiftInterop"),
         .target(
            name: "AppKit",
	    dependencies: ["libobjc2", "AppKitGNUStep", "ObjCSwiftInterop", "FoundationGNUStep"]),
	 .systemLibrary(name: "libobjc2"),
	 .systemLibrary(name: "AppKitGNUStep"),
	 .systemLibrary(name: "FoundationGNUStep"),
      ]
   )

パッケージのPackage.swift

// swift-tools-version: 5.8
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
   name: "AppVulkan",
   products: [
      // Products define the executables and libraries a package produces, making them visible to other packages
      .library(name: "Vulkan", targets: ["Vulkan"]),
      .library(name: "SGLMath", targets: ["SGLMath"]),
   ],
   dependencies: [
      .package(path: "./AppKit")
   ],
   targets: [
      // Targets are the basic building blocks of a package, defining a module or a test suite.
      // Targets can depend on other targets in this package and products from dependencies.
      .executableTarget(
         name: "Main",
	 dependencies: ["AppKit", "CGLFW", "Vulkan", "SGLMath"]
      ),

      .target(name: "Vulkan",
         dependencies: ["CVulkan"]),
      .target(name: "SGLMath"),
		
      .systemLibrary(name: "CVulkan"),
      .systemLibrary(name: "CGLFW"),
   ]
)

/CGLFW, /Cvulkan, /Vulkan はVulkanのサンプルプログラムをそのまま利用して
います。
/SGLMathは、SwiftGL Math Library(https://github.com/SwiftGL/Math)を利用
します。

AppError.swift

// The Swift Programming Language
// https://docs.swift.org/swift-book

struct AppError: Error, CustomStringConvertible {
    let description: String
}

AppDelegate.swift

// The Swift Programming Language
// https://docs.swift.org/swift-book

import Foundation
import AppKit
import ObjCSwiftInterop

let AppWindowRect = CGRect(x: 250, y: 100, width: 600, height: 500)

let fileManager = FileManager.default

class AppDelegate: NSApplicationDelegate {
	lazy var window = NSWindow(CGRect(x: 0, y: 500, width: 200, height: 200))
	lazy var view = UIView()
	lazy var button  = NSButton()
	lazy var button2 = NSButton()
	lazy var button3 = NSButton()

	override func applicationDidFinishLaunching(notification: Any?) {
		window.orderFront(sender: self)
		window.setTitle(NSString(string: "AppKit Test"))

		button.setTitle(NSString(string: "ClearColor"))
		button.frame = .init(x: 30, y: 10, width: 140, height: 32)
		button.onAction = { button in
			AppClearColor().run()
		}
		button2.setTitle(NSString(string: "Triangle"))
		button2.frame = .init(x: 30, y: 50, width: 140, height: 32)
		button2.onAction = { button in
			AppTriangle().run()
		}
		button3.setTitle(NSString(string: "UniformBuffer"))
		button3.frame = .init(x: 30, y: 90, width: 140, height: 32)
		button3.onAction = { button in
			AppUniformBuffer().run()
		}

		view.frame = .init(x: 0, y: 0, width: 300, height: 300)
		view.addSubview(button)
		view.addSubview(button2)
		view.addSubview(button3)
		window.setContentView(view)
	}
}

AppUniformBuffer.swift

import Foundation
import CVulkan
import CGLFW
import Vulkan
import SGLMath

//struct Vector2
//struct Vector3
//struct Vertex
// defined in AppTriangle

class AppUniformBuffer {
    /// A GLFW window handle.
    var window: OpaquePointer!

    /// The size of the window.
    let windowSize = Extent2D(width: UInt32(AppWindowRect.size.width), height: UInt32(AppWindowRect.size.height))
    
    /// Initializes GLFW and creates a window.
    func createWindow() throws {
        guard glfwInit() == GLFW_TRUE else {
            throw AppError(description: "Failed to initialize GLFW.")
        }
        glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API)
        glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE)
        glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE)
        guard let window = glfwCreateWindow(Int32(windowSize.width), Int32(windowSize.height), "Vulkan Triangle", nil, nil) else {
            throw AppError(description: "Failed to create window.")
        }
        glfwSetWindowPos(window, Int32(AppWindowRect.origin.x), Int32(AppWindowRect.origin.y))
	    glfwShowWindow(window)

        self.window = window

        glfwSetKeyCallback(window, key_callback)
    }

    /// A Vulkan instance.
    var instance: Instance!
    /// Creates a Vulkan instance with the extensions required by GLFW.
    func createInstance() throws {
        var count: UInt32 = 0
        let extensionsPtr = glfwGetRequiredInstanceExtensions(&count)
        var extensions = UnsafeBufferPointer(start: extensionsPtr, count: Int(count)).map{ String(cString: $0!) }
        var layers: [String] = []
        
        #if DEBUG
        extensions.append(VK_EXT_DEBUG_UTILS_EXTENSION_NAME)
        layers.append("VK_LAYER_KHRONOS_validation")
        #endif

        let entry = try Entry()
        instance = try entry.createInstance(
            createInfo: .init(
                flags: [],
                applicationInfo: nil,
                enabledLayerNames: layers,
                enabledExtensionNames: extensions
            )
        )
    }

    /// A Vulkan surface.
    var surface: SurfaceKHR!
    /// Creates a surface, via GLFW.
    func createSurface() throws {
        var cSurface: VkSurfaceKHR!
        let result = instance.withHandle { handle in
            glfwCreateWindowSurface(handle, window, nil, &cSurface)
        }
        guard result.rawValue >= 0 else {
            throw Result(rawValue: result.rawValue)!
        }
        surface = SurfaceKHR(handle: cSurface, instance: instance)
    }

    /// A Vulkan physical device.
    var physicalDevice: PhysicalDevice!
    /// The index of a graphics queue family.
    var graphicsFamilyIndex: UInt32!
    /// The index of a presentation queue family.
    var presentationFamilyIndex: UInt32!
    /// Selects a suitable physical device, as well as the graphics and presentation queue families.
    func selectPhysicalDevice() throws {
        let physicalDevices = try instance.getPhysicalDevices()
        
        for physicalDevice in physicalDevices {
            // Device must support the VK_KHR_swapchain extension
            let extensions = try physicalDevice.getDeviceExtensionProperties(layerName: nil)
            guard extensions.contains(where: { $0.extensionName == VK_KHR_SWAPCHAIN_EXTENSION_NAME }) else {
                continue
            }
            
            // Device must support the b8g8r8a8Srgb / srgbNonLinear surface format.
            let surfaceFormats = try physicalDevice.getSurfaceFormatsKHR(surface: surface)
            guard surfaceFormats.contains(where: { $0.format == .b8g8r8a8Srgb && $0.colorSpace == .srgbNonlinear }) else {
                continue
            }
            
            // Device must have a graphics queue family.
            let queueFamilies = physicalDevice.getQueueFamilyProperties()
            guard let graphicsFamilyIndex = queueFamilies.firstIndex(where: { $0.queueFlags.contains(.graphics) }) else {
                continue
            }
            
            // Device must have a presentation queue family that is compatible with the surface.
            let queueFamilyIndices = 0..<UInt32(queueFamilies.count)
            guard let presentationFamilyIndex = try queueFamilyIndices.first(where: {
                try physicalDevice.getSurfaceSupportKHR(queueFamilyIndex: $0, surface: surface)
            }) else {
                continue
            }
            
            self.graphicsFamilyIndex = UInt32(graphicsFamilyIndex)
            self.presentationFamilyIndex = presentationFamilyIndex
            self.physicalDevice = physicalDevice
            break
        }
        
        guard physicalDevice != nil else {
            throw AppError(description: "Could not find a suitable device.")
        }
    }

    /// A Vulkan device.
    var device: Device!
    /// A Vulkan queue that supports graphics commands.
    var graphicsQueue: Queue!
    /// A Vulkan queue that supports presentation commands.
    var presentationQueue: Queue!
    /// Creates a logical device and retrieves its queues.
    func createDevice() throws {
        let families: Set = [graphicsFamilyIndex!, presentationFamilyIndex!]
        
        let queueInfos = families.map {
            DeviceQueueCreateInfo(
                flags: [],
                queueFamilyIndex: UInt32($0),
                queuePriorities: [1.0]
            )
        }
        
        device = try physicalDevice.createDevice(
            createInfo: .init(
                flags: [],
                queueCreateInfos: queueInfos,
                enabledLayerNames: [],
                enabledExtensionNames: [VK_KHR_SWAPCHAIN_EXTENSION_NAME],
                enabledFeatures: nil
            )
        )
        
        graphicsQueue = device.getQueue(queueFamilyIndex: graphicsFamilyIndex, queueIndex: 0)
        presentationQueue = device.getQueue(queueFamilyIndex: presentationFamilyIndex, queueIndex: 0)
    }

    /// A Vulkan swapchain.
    var swapchain: SwapchainKHR!
    /// The size of the swapchain images.
    var swapchainExtent: Extent2D!
    /// An array of Vulkan swapchain images.
    var swapchainImages: [Image] = []
    /// Creates a swapchain and retrieves its images.
    func createSwapchain() throws {
        let capabilities = try physicalDevice.getSurfaceCapabilitiesKHR(surface: surface)
        
        var imageCount = capabilities.minImageCount + 1
        if capabilities.maxImageCount > 0 && imageCount > capabilities.maxImageCount {
            imageCount = capabilities.maxImageCount
        }
        
        if capabilities.currentExtent.width != UInt32.max {
            swapchainExtent = capabilities.currentExtent
        } else {
            let width = max(capabilities.minImageExtent.width, min(capabilities.maxImageExtent.width, windowSize.width))
            let height = max(capabilities.minImageExtent.height, min(capabilities.maxImageExtent.height, windowSize.height))
            swapchainExtent = Extent2D(width: width, height: height)
        }
        
        swapchain = try device.createSwapchainKHR(
            createInfo: .init(
                flags: [],
                surface: surface,
                minImageCount: imageCount,
                imageFormat: .b8g8r8a8Srgb,
                imageColorSpace: .srgbNonlinear,
                imageExtent: swapchainExtent,
                imageArrayLayers: 1,
                imageUsage: .colorAttachment,
                imageSharingMode: graphicsFamilyIndex == presentationFamilyIndex ? .exclusive : .concurrent,
                queueFamilyIndices: [graphicsFamilyIndex, presentationFamilyIndex],
                preTransform: capabilities.currentTransform,
                compositeAlpha: .opaque,
                presentMode: .fifo,
                clipped: true,
                oldSwapchain: nil
            )
        )
        
        swapchainImages = try swapchain.getImagesKHR()
    }
    
    /// An array of Vulkan image views.
    var imageViews: [ImageView] = []
    /// Creates an image view for each of the swapchain images.
    func createImageViews() throws {
        for image in swapchainImages {
            let imageView = try device.createImageView(
                createInfo: .init(
                    flags: [],
                    image: image,
                    viewType: .type2d,
                    format: .b8g8r8a8Srgb,
                    components: .init(r: .r, g: .g, b: .b, a: .a),
                    subresourceRange: .init(
                        aspectMask: .color,
                        baseMipLevel: 0,
                        levelCount: 1,
                        baseArrayLayer: 0,
                        layerCount: 1
                    )
                )
            )
            imageViews.append(imageView)
        }
    }
    
    /// A Vulkan render pass.
    var renderPass: RenderPass!
    /// Creates a Vulkan render pass.
    func createRenderPass() throws {
        let colorAttachment = AttachmentDescription(
            flags: [],
            format: .b8g8r8a8Srgb,
            samples: .type1,
            loadOp: .clear,
            storeOp: .store,
            stencilLoadOp: .dontCare,
            stencilStoreOp: .dontCare,
            initialLayout: .undefined,
            finalLayout: .presentSrcKHR
        )
        
        let colorAttachmentRef = AttachmentReference(attachment: 0, layout: .colorAttachmentOptimal)
        
        let subpass = SubpassDescription(
            flags: [],
            pipelineBindPoint: .graphics,
            inputAttachments: [],
            colorAttachments: [colorAttachmentRef],
            resolveAttachments: nil,
            depthStencilAttachment: nil,
            preserveAttachments: []
        )
        
        let dependency = SubpassDependency(
            srcSubpass: VK_SUBPASS_EXTERNAL,
            dstSubpass: 0,
            srcStageMask: .colorAttachmentOutput,
            dstStageMask: .colorAttachmentOutput,
            srcAccessMask: [],
            dstAccessMask: .colorAttachmentWrite,
            dependencyFlags: []
        )
        
        renderPass = try device.createRenderPass(
            createInfo: .init(
                flags: [],
                attachments: [colorAttachment],
                subpasses: [subpass],
                dependencies: [dependency]
            )
        )
    }
    
    /// Creates a Vulkan shader module from the source file at the given path.
    func createShaderModule(path: String) throws -> ShaderModule {
        guard let buffer = readFile(path: path) else {
            throw AppError(description: "Failed to read file: \(path)")
        }
        
        return try buffer.withUnsafeBytes { bytes in
            try device.createShaderModule(
                createInfo: .init(
                    flags: [],
                    codeSize: buffer.count,
                    code: bytes.bindMemory(to: UInt32.self).baseAddress!
                )
            )
        }
    }
    
    /// DescriptorSetLayout
    var descriptorSetLayout: DescriptorSetLayout!
    func createDescriptorSetLayout() throws {
        let uboLayoutBinding = DescriptorSetLayoutBinding(
            binding: 0,
            descriptorType: .uniformBuffer,
            descriptorCount: 1,
            stageFlags: .vertex,
            immutableSamplers: []
        )

        descriptorSetLayout = try self.device.createDescriptorSetLayout(
            createInfo: .init(
                flags: [],
                bindings: [uboLayoutBinding]
            )
        )
    }

    /// A Vulkan pipeline layout.
    var pipelineLayout: PipelineLayout!
    /// A Vulkan graphics pipeline.
    var pipeline: Pipeline!
    /// Creates a Vulkan graphics pipeline.
    func createGraphicsPipeline() throws {
        let vertexShader = try createShaderModule(path: "./Sources/Main/shaders_ubo/vert.spv")
        let fragmentShader = try createShaderModule(path: "./Sources/Main/shaders_ubo/frag.spv")
        defer {
            vertexShader.destroy()
            fragmentShader.destroy()
        }
        
        let vertexStage = PipelineShaderStageCreateInfo(
            flags: [],
            stage: .vertex,
            module: vertexShader,
            name: "main",
            specializationInfo: nil
        )
        
        let fragmentStage = PipelineShaderStageCreateInfo(
            flags: [],
            stage: .fragment,
            module: fragmentShader,
            name: "main",
            specializationInfo: nil
        )
        
        let vertexBinding = VertexInputBindingDescription(
            binding: 0,
            stride: UInt32(MemoryLayout<Vertex>.stride),
            inputRate: .vertex
        )
        
        let positionAttribute = VertexInputAttributeDescription(
            location: 0,
            binding: 0,
            format: .r32g32Sfloat,
            offset: UInt32(MemoryLayout.offset(of: \Vertex.position)!)
        )
        
        let colorAttribute = VertexInputAttributeDescription(
            location: 1,
            binding: 0,
            format: .r32g32b32Sfloat,
            offset: UInt32(MemoryLayout.offset(of: \Vertex.color)!)
        )
        
        let vertexInputState = PipelineVertexInputStateCreateInfo(
            flags: [],
            vertexBindingDescriptions: [vertexBinding],
            vertexAttributeDescriptions: [positionAttribute, colorAttribute]
        )
        
        let inputAssemblyState = PipelineInputAssemblyStateCreateInfo(
            flags: [],
            topology: .triangleList,
            primitiveRestartEnable: false
        )
        
        let viewport = Viewport(
            x: 0,
            y: 0,
            width: Float(swapchainExtent.width),
            height: Float(swapchainExtent.height),
            minDepth: 0,
            maxDepth: 1
        )
        
        let scissor = Rect2D(offset: .init(x: 0, y: 0), extent: swapchainExtent)
        
        let viewportState = PipelineViewportStateCreateInfo(
            flags: [],
            viewports: [viewport],
            scissors: [scissor]
        )
        
        let rasterizationState = PipelineRasterizationStateCreateInfo(
            flags: [],
            depthClampEnable: false,
            rasterizerDiscardEnable: false,
            polygonMode: .fill,
            cullMode: .back,
            frontFace: .clockwise,
            depthBiasEnable: false,
            depthBiasConstantFactor: 0,
            depthBiasClamp: 0,
            depthBiasSlopeFactor: 0,
            lineWidth: 1
        )
        
        let multisampleState = PipelineMultisampleStateCreateInfo(
            flags: [],
            rasterizationSamples: .type1,
            sampleShadingEnable: false,
            minSampleShading: 0,
            sampleMask: nil,
            alphaToCoverageEnable: false,
            alphaToOneEnable: false
        )
        
        let colorBlendAttachmentState = PipelineColorBlendAttachmentState(
            blendEnable: false,
            srcColorBlendFactor: .zero,
            dstColorBlendFactor: .zero,
            colorBlendOp: .add,
            srcAlphaBlendFactor: .zero,
            dstAlphaBlendFactor: .zero,
            alphaBlendOp: .add,
            colorWriteMask: [.r, .g, .b, .a]
        )
        
        let colorBlendState = PipelineColorBlendStateCreateInfo(
            flags: [],
            logicOpEnable: false,
            logicOp: .copy,
            attachments: [colorBlendAttachmentState],
            blendConstants: (0, 0, 0, 0)
        )
        
        pipelineLayout = try device.createPipelineLayout(
            createInfo: .init(
                flags: [],
                setLayouts: [self.descriptorSetLayout],
                pushConstantRanges: []
            )
        )
        
        let pipelineInfo = GraphicsPipelineCreateInfo(
            flags: [],
            stages: [vertexStage, fragmentStage],
            vertexInputState: vertexInputState,
            inputAssemblyState: inputAssemblyState,
            tessellationState: nil,
            viewportState: viewportState,
            rasterizationState: rasterizationState,
            multisampleState: multisampleState,
            depthStencilState: nil,
            colorBlendState: colorBlendState,
            dynamicState: nil,
            layout: pipelineLayout,
            renderPass: renderPass,
            subpass: 0,
            basePipelineHandle: nil,
            basePipelineIndex: 0
        )
        
        pipeline = try device.createGraphicsPipelines(
            pipelineCache: nil,
            createInfos: [pipelineInfo]
        )[0]
    }
    
    /// An array of Vulkan framebuffers.
    var framebuffers: [Framebuffer] = []
    /// Creates a Vulkan framebuffer for each of the swapchain image views.
    func createFramebuffers() throws {
        for imageView in imageViews {
            let framebuffer = try device.createFramebuffer(
                createInfo: .init(
                    flags: [],
                    renderPass: renderPass,
                    attachments: [imageView],
                    width: swapchainExtent.width,
                    height: swapchainExtent.height,
                    layers: 1
                )
            )
            framebuffers.append(framebuffer)
        }
    }
    
    /// Creates a Vulkan buffer and allocates memory for it, with the given properties.
    func createBuffer(size: VkDeviceSize, usage: BufferUsageFlags, properties: MemoryPropertyFlags) throws -> (Buffer, DeviceMemory) {
        let buffer = try device.createBuffer(
            createInfo: .init(
                flags: [],
                size: size,
                usage: usage,
                sharingMode: .exclusive,
                queueFamilyIndices: []
            )
        )
        
        let memoryRequirements = buffer.getMemoryRequirements()
        let memoryProperties = physicalDevice.getMemoryProperties()
        
        guard let memoryType = memoryProperties.memoryTypes.enumerated().first(where: { index, type in
            (memoryRequirements.memoryTypeBits & (1 << index) != 0) &&
                type.propertyFlags.contains(properties)
        }) else {
            throw AppError(description: "Could not find suitable memory type.")
        }
        
        let memory = try device.allocateMemory(
            allocateInfo: .init(
                allocationSize: memoryRequirements.size,
                memoryTypeIndex: UInt32(memoryType.offset)
            )
        )
        
        try buffer.bindMemory(memory: memory, memoryOffset: 0)
        
        return (buffer, memory)
    }
    
    /// An array of vertices that describe a triangle.
    let vertices = [
        Vertex(position: .init(x: 0, y: -0.5), color: .init(x: 1, y: 0, z: 0)),
        Vertex(position: .init(x: 0.5, y: 0.5), color: .init(x: 0, y: 1, z: 0)),
        Vertex(position: .init(x: -0.5, y: 0.5), color: .init(x: 0, y: 0, z: 1))
    ]
    
    /// The size, in bytes, of the vertices.
    var bufferSize: VkDeviceSize!
    /// A Vulkan buffer used to stage the vertices before transfering them to the GPU.
    var stagingBuffer: Buffer!
    /// Host visible memory that is bound to the staging buffer.
    var stagingBufferMemory: DeviceMemory!
    /// A Vulkan buffer used to store the vertices that will be accessed by the GPU.
    var vertexBuffer: Buffer!
    /// Device local memory that is bound to the vertex buffer.
    var vertexBufferMemory: DeviceMemory!
    /// Creates the staging and vertex buffers.
    func createVertexBuffer() throws {
        bufferSize = VkDeviceSize(MemoryLayout<Vertex>.stride * vertices.count)
        
        (stagingBuffer, stagingBufferMemory) = try createBuffer(size: bufferSize, usage: .transferSrc, properties: [.hostVisible, .hostCoherent])
        
        (vertexBuffer, vertexBufferMemory) = try createBuffer(size: bufferSize, usage: [.vertexBuffer, .transferDst], properties: .deviceLocal)
    }

    /// create UniformData
    var model: mat4 = mat4(0.0)
    var view: mat4 = mat4(0.0)
    var proj: mat4 = mat4(0.0)
    func createUniformData() {
        self.proj = SGLMath.perspective(radians(45), Float(1.0), Float(0.1), Float(10))
        self.view = SGLMath.lookAt(
            vec3(2.0, 2.0, 1.0),
            vec3(0, 0, 0),
            vec3(0, 0, 1))

    //    let angle = Float.pi * 0.5
        self.model = SGLMath.rotate(mat4(1.0), 0.0, vec3(0, 0, 1))
    }

    /// add UniformBuffer DescriptorSet
    struct UniformBufferObject{
        var model: mat4
        var view: mat4
        var proj: mat4
    }

    var uboBufferSize: VkDeviceSize!
    var uniformBuffer: Buffer!
    var uniformBufferMemory: DeviceMemory!
    func createUniformBuffer() throws {
        //struct UniformBufferObject{
        //    var model: mat4
        //    var view: mat4
        //    var proj: mat4
        //}
        let ubo = UniformBufferObject(model: self.model, view: self.view, proj: self.proj)
        print("ubo", ubo)

        uboBufferSize = VkDeviceSize(MemoryLayout.size(ofValue: ubo))
        print("uniform bufferSize", uboBufferSize)
        (uniformBuffer, uniformBufferMemory) = try createBuffer(size: uboBufferSize, usage: .uniformBuffer, properties: [.hostVisible, .hostCoherent])
        //print("uniformBuffer", uniformBuffer)

        //let ptr = try uniformBufferMemory.mapMemory(offset: 0, size: uboBufferSize, flags: [])
        //withUnsafeBytes(of: ubo) { bytes in
        //    ptr.copyMemory(from: bytes.baseAddress!, byteCount: bytes.count)
        //}
        //uniformBufferMemory.unmapMemory()
    }

    var counter: Float = 0
    func updateUniformBuffer() throws {
        //struct UniformBufferObject{
        //    var model: mat4
        //    var view: mat4
        //    var proj: mat4
        //}

        //var model: mat4 = mat4(0.0)
        //var view: mat4 = mat4(0.0)
        //var proj: mat4 = mat4(0.0)
    
        //let proj = SGLMath.perspective(radians(45), Float(1.0), Float(0.1), Float(10))
        //let view = SGLMath.lookAt(
        //    vec3(2.0, 2.0, 2.0),
        //    vec3(0, 0, 0),
        //    vec3(0, 0, 1))

        counter += 0.1
        var angle = counter
        let model = SGLMath.rotate(mat4(1.0), angle, vec3(0, 0, 1))

        let ubo = UniformBufferObject(model: model, view: self.view, proj: self.proj)
        //print("ubo", ubo)

        //uboBufferSize = VkDeviceSize(MemoryLayout.size(ofValue: ubo))
        //print("uniform bufferSize", uboBufferSize)
        //(uniformBuffer, uniformBufferMemory) = try createBuffer(size: uboBufferSize, usage: .uniformBuffer, properties: [.hostVisible, .hostCoherent])
        //print("uniformBuffer", uniformBuffer)

        let ptr = try uniformBufferMemory.mapMemory(offset: 0, size: uboBufferSize, flags: [])
        withUnsafeBytes(of: ubo) { bytes in
            ptr.copyMemory(from: bytes.baseAddress!, byteCount: bytes.count)
        }
        uniformBufferMemory.unmapMemory()
    }

    /// DescriptorPool, DescriptorSet
    var descriptorPool: DescriptorPool!
    var descriptorSets: [DescriptorSet] = []
    func createDescriptorSet() throws {
        let poolSize = DescriptorPoolSize(
                type: .uniformBuffer,
                descriptorCount: 1
        )

        let info = DescriptorPoolCreateInfo(
                flags: [],
                maxSets: 1,
                poolSizes: [poolSize]
        )

        descriptorPool = try self.device.createDescriptorPool(createInfo: info)

        let allocInfo = DescriptorSetAllocateInfo(
            descriptorPool: descriptorPool,
            setLayouts: [self.descriptorSetLayout]
        )

        descriptorSets = try self.device.allocateDescriptorSets(allocateInfo: allocInfo)
        print("descriptorSets", descriptorSets)

        let bufferInfo = DescriptorBufferInfo(
            buffer: uniformBuffer,
            offset: 0,
            range: UInt64(self.uboBufferSize)
        )

        let wds = WriteDescriptorSet(
            dstSet: descriptorSets[0],
            dstBinding: 0,
            dstArrayElement: 0,
            descriptorType: .uniformBuffer,
            descriptorCount: 1,
            imageInfo: [],
            bufferInfo: [bufferInfo],
            texelBufferView: []
        )

        self.device.updateDescriptorSets(descriptorWrites: [wds], descriptorCopies: [])
    }

    /// A Vulkan command pool.
    var commandPool: CommandPool!
    /// Creates a command pool.
    func createCommandPool() throws {
        commandPool = try device.createCommandPool(
            createInfo: .init(
                flags: [.resetCommandBuffer],
                queueFamilyIndex: graphicsFamilyIndex
            )
        )
    }
    
    /// Copies the vertices to the staging buffer, then transfers the contents of the staging buffer to the vertex buffer.
    func copyVertexBuffer() throws {
        let ptr = try stagingBufferMemory.mapMemory(offset: 0, size: bufferSize, flags: [])
        vertices.withUnsafeBytes { bytes in
            ptr.copyMemory(from: bytes.baseAddress!, byteCount: bytes.count)
        }
        stagingBufferMemory.unmapMemory()
        
        let commandBuffer = try device.allocateCommandBuffers(
            allocateInfo: .init(
                commandPool: commandPool,
                level: .primary,
                commandBufferCount: 1
            )
        )[0]
        defer {
            commandPool.freeCommandBuffers(commandBuffers: [commandBuffer])
        }
        
        try commandBuffer.begin(
            beginInfo: .init(
                flags: .oneTimeSubmit,
                inheritanceInfo: nil
            )
        )
        
        let copyRegion = BufferCopy(srcOffset: 0, dstOffset: 0, size: bufferSize)
        commandBuffer.cmdCopyBuffer(srcBuffer: stagingBuffer, dstBuffer: vertexBuffer, regions: [copyRegion])
        
        try commandBuffer.end()
        
        let submitInfo = SubmitInfo(
            waitSemaphores: [],
            waitDstStageMask: [],
            commandBuffers: [commandBuffer],
            signalSemaphores: []
        )
        try graphicsQueue.submit(submits: [submitInfo], fence: nil)
        try graphicsQueue.waitIdle()
    }

    /// A Vulkan command buffer.
    var commandBuffers: [CommandBuffer] = []
    /// Allocates a command buffer for each of the swapchain images.
    func createCommandBuffers() throws {
        commandBuffers = try device.allocateCommandBuffers(
            allocateInfo: .init(
                commandPool: commandPool,
                level: .primary,
                commandBufferCount: UInt32(swapchainImages.count)
            )
        )
    }
    
    /// Records a set of commands for each command buffer.
    func recordCommandBuffers() throws {
        for (commandBuffer, framebuffer) in zip(commandBuffers, framebuffers) {
            try commandBuffer.begin(beginInfo: .init(flags: [], inheritanceInfo: nil))
            
            let clearColor = VkClearValue(color: .init(float32: (0, 0, 0, 1)))
            commandBuffer.cmdBeginRenderPass(
                renderPassBegin: .init(
                    renderPass: renderPass,
                    framebuffer: framebuffer,
                    renderArea: .init(offset: .init(x: 0, y: 0), extent: swapchainExtent),
                    clearValues: [clearColor]),
                contents: .inline
            )
            
            commandBuffer.cmdBindPipeline(pipelineBindPoint: .graphics, pipeline: pipeline)
            //
            commandBuffer.cmdBindDescriptorSets(pipelineBindPoint: .graphics, layout: self.pipelineLayout, firstSet: 0, descriptorSets: self.descriptorSets, dynamicOffsets: [])
            //
            commandBuffer.cmdBindVertexBuffers(firstBinding: 0, buffers: [vertexBuffer], offsets: [0])
            commandBuffer.cmdDraw(vertexCount: UInt32(vertices.count), instanceCount: 1, firstVertex: 0, firstInstance: 0)
            
            commandBuffer.cmdEndRenderPass()
            
            try commandBuffer.end()
        }
    }

    /// A semaphore to signal when an image is available for rendering.
    var imageAvailable: Vulkan.Semaphore!
    /// A semaphore to signal when rendering is complete.
    var renderComplete: Vulkan.Semaphore!
    /// Creates any objects required for synchronization.
    func createSync() throws {
        imageAvailable = try device.createSemaphore(createInfo: .init(flags: []))
        renderComplete = try device.createSemaphore(createInfo: .init(flags: []))
    }
    
    /// Aquires an image from the swapchain, submits the associated command buffer, and finally presents the image.
    func drawFrame() throws {
        try updateUniformBuffer()

        let index = try swapchain.acquireNextImageKHR(timeout: UInt64.max, semaphore: imageAvailable, fence: nil)
        
        let graphicsSubmit = SubmitInfo(
            waitSemaphores: [imageAvailable],
            waitDstStageMask: [.colorAttachmentOutput],
            commandBuffers: [commandBuffers[Int(index)]],
            signalSemaphores: [renderComplete]
        )
        try graphicsQueue.submit(submits: [graphicsSubmit], fence: nil)
        
        try presentationQueue.presentKHR(
            presentInfo: .init(
                waitSemaphores: [renderComplete],
                swapchains: [swapchain],
                imageIndices: [index],
                results: nil
            )
        )
        
        // Note: Don't use this in the real world. It is not an efficient method of synchronization.
        try presentationQueue.waitIdle()
    }

    /// Runs the main draw loop until the window is closed.
    func mainLoop() throws {
        while glfwWindowShouldClose(window) == GLFW_FALSE {
            glfwPollEvents()
            try drawFrame()
        }
        try device.waitIdle()
    }
    
    /// Initializes and runs the application.
    func run() {
        do {
            try createWindow()
            try createInstance()
            try createSurface()
            try selectPhysicalDevice()
            try createDevice()
            try createSwapchain()
            try createImageViews()
            try createRenderPass()
            try createDescriptorSetLayout()
            try createGraphicsPipeline()
            try createFramebuffers()
            try createVertexBuffer()
            try createCommandPool()
            try copyVertexBuffer()
            createUniformData()
            try createUniformBuffer()
            try createDescriptorSet()
            try createCommandBuffers()
            try recordCommandBuffers()
            try createSync()
            try mainLoop()
        } catch let e as Result {
            print("Encountered Vulkan error: \(e)")
        } catch let e {
            print(e)
        }
    }

    /// Cleans up any resources that were created.
    deinit {
        imageAvailable?.destroy()
        renderComplete?.destroy()
        commandPool?.destroy()
        vertexBuffer?.destroy()
        vertexBufferMemory?.freeMemory()
        stagingBuffer?.destroy()
        stagingBufferMemory?.freeMemory()
        //
        uniformBuffer?.destroy()
        uniformBufferMemory?.freeMemory()
        //
        for framebuffer in framebuffers { framebuffer.destroy() }
        pipeline?.destroy()
        pipelineLayout?.destroy()
        //
        descriptorSetLayout?.destroy()
        descriptorPool?.destroy()
        //
        renderPass?.destroy()
        for imageView in imageViews { imageView.destroy() }
        swapchain?.destroyKHR()
        device?.destroy()
        surface?.destroyKHR()
        instance?.destroy()
        glfwDestroyWindow(window)
        glfwTerminate()
    }
}

/// GLFW key callback
// defined in AppClearColor

shaderは次のようになります。

/shaders_ubo
shader.vert
#version 450

layout (binding = 0) uniform bufferVals {
    mat4 model;
    mat4 view;
    mat4 proj;
} ubo;

layout (location = 0) in vec2 inPosition;
layout (location = 1) in vec3 inColor;

layout (location = 0) out vec3 fragColor;

void main() {
   vec4 pos = vec4(inPosition, 0.0, 1.0);

   gl_Position = ubo.proj * ubo.view * ubo.model * pos;
   fragColor = inColor;
}

shader.frag
#version 450

layout (location = 0) in vec3 fragColor;
layout (location = 0) out vec4 outColor;
void main() {
   outColor = vec4(fragColor, 1.0);
}

glslang でspvに変換します。