Dominaezzz / kgl

Thin multiplatform wrappers for graphics.
Apache License 2.0
105 stars 13 forks source link
glfw3 graphics kgl kotlin kotlin-multiplatform kotlin-native opengl vulkan vulkan-api

GitHub license

Kotlin Graphics Libraries

Kotlin Multiplatform libraries for graphics.

KGL uses LWJGL for the JVM target and the respective native libraries on the native targets. It provides a thin OOP wrapper with DSLs to make programming with vulkan easier.

You can find some kgl-vulkan samples here and kgl-opengl samples here.

Usage

repositories {
    maven("https://maven.pkg.github.com/Dominaezzz/kgl") {
        credentials {
            username = System.getenv("GITHUB_USER") // Your GitHub username.
            password = System.getenv("GITHUB_TOKEN") // A GitHub token with `read:packages`.
        }
    }
}

dependencies {
    api("com.kgl:kgl-core:$kglVersion")
    api("com.kgl:kgl-glfw:$kglVersion")
    api("com.kgl:kgl-glfw-static:$kglVersion") // For GLFW static binaries
    api("com.kgl:kgl-opengl:$kglVersion")
    api("com.kgl:kgl-vulkan:$kglVersion")
    api("com.kgl:kgl-glfw-vulkan:$kglVersion")
    api("com.kgl:kgl-stb:$kglVersion")
}

Design

The main goal of this library is to hide the verbosity of working with vulkan.

For example in C++, to create a vulkan instance one would have to write code like,

std::vector<std::string> layers = TODO();
std::vector<std::string> extensions = TODO();

VkApplicationInfo applicationInfo = {};
applicationInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
applicationInfo.pNext = nullptr;
applicationInfo.pApplicationName = "Kgl App";
applicationInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
applicationInfo.pEngineName = "No Engine yet";
applicationInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
applicationInfo.apiVersion = VK_VERSION_1_1;

VkInstanceCreateInfo createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
createInfo.pNext = nullptr;
createInfo.flags = 0;
createInfo.pApplicationInfo = &applicationInfo;
createInfo.enabledLayerCount = layers.size();
createInfo.ppEnabledLayerNames = layers.data();
createInfo.enabledExtensionCount = extensions.size();
createInfo.ppEnabledExtensionNames = extensions.data();

VkInstance instance;
if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) {
    throw std::runtime_error("Failed to create instance!");
}

but in Kotlin (with the help of KGL),

val layers: List<String> = TODO()
val extensions: List<String> = TODO()

val instance = Instance.create(layers, extensions) {
    applicationInfo {
        applicationName = "Kgl App"
        applicationVersion = VkVersion(1u, 1u, 0u)
        engineName = "No engine yet"
        engineVersion = VkVersion(1u, 0u, 0u)
        apiVersion = VkVersion(1u, 1u, 0u)
    }
}

To create a device in C++,

uint32_t deviceCount = 1;
VkPhysicalDevice physicalDevice;
vkEnumeratePhysicalDevices(instance, &deviceCount, &physicalDevice);
if (deviceCount < 1) throw std::runtime_error("Failed to find GPU with vulkan support.");

std::vector<VkDeviceQueueCreateInfo> queueCreateInfos(2);

float queuePriority = 1.0f;

VkDeviceQueueCreateInfo& queueCreateInfo1 = queueCreateInfos[0];
queueCreateInfo1.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
queueCreateInfo1.pNext = nullptr;
queueCreateInfo1.flags = VK_DEVICE_QUEUE_CREATE_PROTECTED;
queueCreateInfo1.queueFamilyIndex = 1;
queueCreateInfo1.queueCount = 1;
queueCreateInfo1.pQueuePriorities = &queuePriority;

VkDeviceQueueCreateInfo& queueCreateInfo2 = queueCreateInfos[1];
queueCreateInfo2.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
queueCreateInfo2.pNext = nullptr;
queueCreateInfo2.flags = 0;
queueCreateInfo2.queueFamilyIndex = 2;
queueCreateInfo2.queueCount = 1;
queueCreateInfo2.pQueuePriorities = &queuePriority;

VkPhysicalDeviceFeatures deviceFeatures = {};
deviceFeatures.samplerAnisotropy = VK_TRUE;
deviceFeatures.geometryShader = VK_TRUE;
deviceFeatures.depthClamp = VK_TRUE;

std::vector<std::string> layers = TODO();
std::vector<std::string> extensions = TODO();

VkDeviceCreateInfo createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
createInfo.pNext = nullptr;
createInfo.pQueueCreateInfos = queueCreateInfos.data();
createInfo.queueCreateInfoCount = queueCreateInfos.size();
createInfo.pEnabledFeatures = &deviceFeatures;
createInfo.enabledExtensionCount = extensions.size();
createInfo.ppEnabledExtensionNames = extensions.data();
createInfo.enabledLayerCount = layers.size();
createInfo.ppEnabledLayerNames = layers.data();

VkDevice device;
if (vkCreateDevice(physicalDevice, &createInfo, nullptr, &device) != VK_SUCCESS) {
    throw std::runtime_error("failed to create logical device!");
}

but in Kotlin,

val physicalDevice = instance.physicalDevices.first()

val device = physicalDevice.createDevice(layers, extensions) {
    queues {
        queue(1u, 1.0f) {
            flags = DeviceQueueCreate.PROTECTED
        }

        queue(2u, 1.0f)
    }

    enabledFeatures {
        samplerAnisotropy = true
        geometryShader = true
        depthClamp = true
    }
}

Handles

Every vulkan handle has a class of it's own. The name of the class being the name of the handle without the Vk prefix. Like Instance for VkInstance, CommandBuffer for VkCommandBuffer, etc. All handles keep a reference to their parent, to be able to destroy or free themselves later. Some handles hold a few values from their creation. Like Image has size, layers and format properies.

Structs

Input structs on the other hand have a DSL builder class. Output structs have a corresponding data class.

typedef struct VkLayerProperties {
    char        layerName[VK_MAX_EXTENSION_NAME_SIZE];
    uint32_t    specVersion;
    uint32_t    implementationVersion;
    char        description[VK_MAX_DESCRIPTION_SIZE];
} VkLayerProperties;
data class LayerProperties(
    val layerName: String,
    val specVersion: VkVersion,
    val implementationVersion: UInt,
    val description: String
)

Enums

Enums are represented with a kotlin enum. If the enum is a part a bitmask then it extends VkFlag<T> to allow for type-safe bit fiddling.

typedef VkFlags VkCullModeFlags;
typedef enum VkCullModeFlagBits {
    VK_CULL_MODE_NONE = 0,
    VK_CULL_MODE_FRONT_BIT = 0x00000001,
    VK_CULL_MODE_BACK_BIT = 0x00000002,
    VK_CULL_MODE_FRONT_AND_BACK = 0x00000003,
} VkCullModeFlagBits;

VkCullModeFlags flags = VK_CULL_MODE_FRONT_BIT | VK_CULL_MODE_BACK_BIT;
enum class CullMode : VkFlag<CullMode> {
    NONE,
    FRONT,
    BACK
}

val flags: VkFlag<CullMode> = CullMode.FRONT or CullMode.BACK

Although this does mean that bitwise operations create new objects. Once inline enums are implemented in Kotlin, we'll get the type-safety without the allocations.

Commands

Every command's function pointer has been explicitly loaded using vkGetDeviceProcAddr and vkGetInstanceProcAddr for optimal command calling performance. This also means you don't need to have the Vulkan SDK installed to get started with kgl-vulkan.

At the moment every core and extension (non-platform specific) command has been implemented as a member function/property of a handle class. In most cases it is a member of the last of the first consecutive handles in the parameter list. In other cases, it is moved to a handle class that makes the most sense.

VkResult vkMapMemory(VkDevice device, VkDeviceMemory memory, VkDeviceSize offset, VkDeviceSize size, VkMemoryMapFlags flags, void** ppData);
fun DeviceMemory.map(offset: ULong, size: ULong, flags: VkFlag<MemoryMap>? = null): IoBuffer

or

VkResult vkEnumeratePhysicalDevices(VkInstance instance, uint32_t* pPhysicalDeviceCount, VkPhysicalDevice* pPhysicalDevices);
val Instance.physicalDevices: List<PhysicalDevice>

GLFW Example

val window = Window(1080, 720, "Sample!") {
    clientApi = ClientApi.None
    resizable = false
    visible = true
}

val (width, height) = window.size
val mode = Glfw.primaryMonitor!!.videoMode
window.position = ((mode.width - width) / 2) to ((mode.height - height) / 2)

Limitations