diff tools/vulkan_api_generator/vulkan_api_generator.nim @ 1038:e267983f5edf

did: cleanup
author sam <sam@basx.dev>
date Fri, 22 Mar 2024 11:42:27 +0700
parents generators/vulkan_api/vulkan_api_generator.nim@c66503386e8b
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tools/vulkan_api_generator/vulkan_api_generator.nim	Fri Mar 22 11:42:27 2024 +0700
@@ -0,0 +1,677 @@
+import std/os
+import std/sugar
+import std/algorithm
+import std/strformat
+import std/strutils
+import std/sequtils
+import std/streams
+import std/tables
+import httpClient
+import std/xmlparser
+import std/xmltree
+
+const
+  TYPEMAP = {
+    "void": "void",
+    "char": "char",
+    "float": "float32",
+    "double": "float64",
+    "int8_t": "int8",
+    "uint8_t": "uint8",
+    "int16_t": "int16",
+    "uint16_t": "uint16",
+    "int32_t": "int32",
+    "uint32_t": "uint32",
+    "uint64_t": "uint64",
+    "int64_t": "int64",
+    "size_t": "csize_t",
+    "int": "cint",
+    "void*": "pointer",
+    "char*": "cstring",
+    "ptr char": "cstring",
+    "ptr void": "pointer",
+    "VK_DEFINE_HANDLE": "VkHandle",
+    "VK_DEFINE_NON_DISPATCHABLE_HANDLE": "VkNonDispatchableHandle",
+  }.toTable
+  PLATFORM_HEADER_MAP = {
+    "X11/Xlib.h": @["xlib", "xlib_xrandr"],
+    "X11/extensions/Xrandr.h": @["xlib_xrandr"],
+    "wayland-client.h": @["wayland"],
+    "windows.h": @["win32"],
+    "xcb/xcb.h": @["xcb"],
+    "directfb.h": @["directfb"],
+    "zircon/types.h": @["fuchsia"],
+    "ggp_c/vulkan_types.h": @["ggp"],
+    "screen/screen.h": @["screen"],
+    "nvscisync.h": @["sci"],
+    "nvscibuf.h": @["sci"],
+    "vk_video/vulkan_video_codec_h264std.h": @["provisional"],
+    "vk_video/vulkan_video_codec_h264std_decode.h": @["provisional"],
+    "vk_video/vulkan_video_codec_h264std_encode.h": @["provisional"],
+    "vk_video/vulkan_video_codec_h265std.h": @["provisional"],
+    "vk_video/vulkan_video_codec_h265std_decode.h": @["provisional"],
+    "vk_video/vulkan_video_codec_h265std_encode.h": @["provisional"],
+  }.toTable
+  MAP_KEYWORD = {
+    "object": "theobject",
+    "type": "thetype",
+  }.toTable
+  SPECIAL_DEPENDENCIES = {
+    "VK_NV_ray_tracing": "VK_KHR_ray_tracing_pipeline",
+  }.toTable
+  # will be directly loaded at startup
+  IGNORED_COMMANDS = @["vkGetInstanceProcAddr"]
+  # can be loaded without a vulkan instance
+  GLOBAL_COMMANDS = @[
+    "vkEnumerateInstanceVersion",
+    "vkEnumerateInstanceExtensionProperties",
+    "vkEnumerateInstanceLayerProperties",
+    "vkCreateInstance",
+  ]
+
+# helpers
+func mapType(typename: string): auto =
+  TYPEMAP.getOrDefault(typename.strip(), typename.strip()).strip(chars={'_'})
+func mapName(thename: string): auto =
+  MAP_KEYWORD.getOrDefault(thename.strip(), thename.strip()).strip(chars={'_'})
+func smartParseInt(value: string): int =
+  if value.startsWith("0x"):
+    parseHexInt(value)
+  else:
+    parseInt(value)
+func hasAttr(node: XmlNode, attr: string): bool = node.attr(attr) != ""
+func tableSorted(table: Table[int, string]): seq[(int, string)] =
+  result = toSeq(table.pairs)
+  result.sort((a, b) => cmp(a[0], b[0]))
+func findType(declNode: XmlNode): string =
+  # examples:
+  # char** -> cstringArray
+  # void* -> pointer
+  # char* -> cstring
+  #
+  # int* -> ptr int
+  # void** -> ptr pointer
+  # int** -> ptr ptr int
+  var basetype = ""
+  var apointer = ""
+  var arraylen = ""
+  for child in declNode:
+    if child.kind == xnText:
+        if "[" in child.text:
+          if "[" in child.text and "]" in child.text:
+            arraylen = child.text.strip(chars={'[', ']'}).replace("][", "*")
+          else:
+            arraylen = declNode.child("enum")[0].text
+        else:
+          for i in 0 ..< child.text.count('*'):
+            apointer = apointer & "ptr "
+    elif child.tag == "type":
+      basetype = mapType(child[0].text)
+  if basetype == "void":
+    if apointer.count("ptr ") > 0:
+      basetype = "pointer"
+      apointer = apointer[0 ..< ^4]
+  elif basetype == "char":
+    if apointer.count("ptr ") == 1:
+      basetype = "cstring"
+      apointer = ""
+    elif apointer.count("ptr ") == 2:
+      basetype = "cstringArray"
+      apointer = ""
+    elif apointer.count("ptr ") > 2:
+      basetype = "cstringArray"
+      apointer = apointer[0 ..< ^8]
+
+  result = &"{apointer}{basetype}"
+  if arraylen != "":
+    result = &"array[{arraylen}, {result}]"
+
+# serializers
+# return values and whether this is a bitfield
+func serializeEnum(node: XmlNode, api: XmlNode): (seq[string], string) =
+  let name = node.attr("name")
+  if name == "":
+    return result
+
+  var reservedNames: seq[string]
+  for t in api.findAll("type"):
+    reservedNames.add t.attr("name").replace("_", "").toLower()
+
+  # find additional enum defintion in feature definitions
+  var values: Table[int, string]
+  for feature in api.findAll("feature"):
+    for require in feature.findAll("require"):
+      for theenum in require.findAll("enum"):
+        if theenum.attr("extends") == name:
+          if theenum.hasAttr("offset"):
+            let enumBase = 1000000000 + (smartParseInt(theenum.attr("extnumber")) - 1) * 1000
+            var value = smartParseInt(theenum.attr("offset")) + enumBase
+            if theenum.attr("dir") == "-":
+              value = -value
+            values[value] = theenum.attr("name")
+          elif theenum.hasAttr("value"):
+            var value = smartParseInt(theenum.attr("value"))
+            if theenum.attr("dir") == "-":
+              value = -value
+            values[value] = theenum.attr("name")
+          elif theenum.hasAttr("bitpos"):
+            var value = smartParseInt(theenum.attr("bitpos"))
+            if theenum.attr("dir") == "-":
+              value = -value
+            values[value] = theenum.attr("name")
+          elif theenum.hasAttr("alias"):
+            discard
+          else:
+            raise newException(Exception, &"Unknown extension value: {feature}\nvalue:{theenum}")
+  # find additional enum defintion in extension definitions
+  for extension in api.findAll("extension"):
+    let extensionNumber = parseInt(extension.attr("number"))
+    let enumBase = 1000000000 + (extensionNumber - 1) * 1000
+    for require in extension.findAll("require"):
+      for theenum in require.findAll("enum"):
+        if theenum.attr("extends") == name:
+          if theenum.hasAttr("offset"):
+            if theenum.hasAttr("extnumber"):
+              let otherBase = 1000000000 + (smartParseInt(theenum.attr("extnumber")) - 1) * 1000
+              var value = smartParseInt(theenum.attr("offset")) + otherBase
+              if theenum.attr("dir") == "-":
+                value = -value
+              values[value] = theenum.attr("name")
+            else:
+              var value = smartParseInt(theenum.attr("offset")) + enumBase
+              if theenum.attr("dir") == "-":
+                value = -value
+              values[value] = theenum.attr("name")
+          elif theenum.hasAttr("value"):
+            var value = smartParseInt(theenum.attr("value"))
+            if theenum.attr("dir") == "-":
+              value = -value
+            values[value] = theenum.attr("name")
+          elif theenum.hasAttr("bitpos"):
+            var value = smartParseInt(theenum.attr("bitpos"))
+            if theenum.attr("dir") == "-":
+              value = -value
+            values[value] = theenum.attr("name")
+          elif theenum.hasAttr("alias"):
+            discard
+          else:
+            raise newException(Exception, &"Unknown extension value: {extension}\nvalue:{theenum}")
+
+  # generate enums
+  if node.attr("type") == "enum":
+    for value in node.findAll("enum"):
+      if value.hasAttr("alias"):
+        continue
+      if value.attr("value").startsWith("0x"):
+        values[parseHexInt(value.attr("value"))] = value.attr("name")
+      else:
+        values[smartParseInt(value.attr("value"))] = value.attr("name")
+    if values.len > 0:
+      result[0].add "  " & name & "* {.size: sizeof(cint).} = enum"
+      for (value, name) in tableSorted(values):
+        var thename = name
+        if name.replace("_", "").toLower() in reservedNames:
+          thename = thename & "_ENUM"
+        let enumEntry = &"    {thename} = {value}"
+        result[0].add enumEntry
+
+  # generate bitsets (normal enums in the C API, but bitfield-enums in Nim)
+  elif node.attr("type") == "bitmask":
+    var predefined_enum_sets: seq[string]
+    for value in node.findAll("enum"):
+      if value.hasAttr("bitpos"):
+        values[smartParseInt(value.attr("bitpos"))] = value.attr("name")
+      elif node.attr("name") == "VkVideoEncodeRateControlModeFlagBitsKHR": # special exception, for some reason this has values instead of bitpos
+        values[smartParseInt(value.attr("value"))] = value.attr("name")
+      elif value.hasAttr("value"): # create a const that has multiple bits set
+        predefined_enum_sets.add &"  {value.attr(\"name\")}* = {value.attr(\"value\")}"
+
+    if values.len > 0:
+      let cApiName = name.replace("FlagBits", "Flags")
+      result[1] = cApiName
+      if node.hasAttr("bitwidth"):
+        result[0].add "  " & name & "* {.size: 8.} = enum"
+      else:
+        result[0].add "  " & name & "* {.size: sizeof(cint).} = enum"
+      for (bitpos, enumvalue) in tableSorted(values):
+        var value = "00000000000000000000000000000000"# makes the bit mask nicely visible
+        if node.hasAttr("bitwidth"): # assumes this is always 64
+          value = value & value
+        value[^(bitpos + 1)] = '1'
+        let enumEntry = &"    {enumvalue} = 0b{value}"
+        if not (enumEntry in result[0]): # the specs define duplicate entries for backwards compat
+          result[0].add enumEntry
+      if node.hasAttr("bitwidth"): # assuming this attribute is always 64
+        if values.len > 0:
+          result[0].add &"""func toBits*(flags: openArray[{name}]): {cApiName} =
+    for flag in flags:
+      result = {cApiName}(uint64(result) or uint64(flag))"""
+          result[0].add &"""func toEnums*(number: {cApiName}): seq[{name}] =
+    for value in {name}.items:
+      if (cast[uint64](value) and uint64(number)) > 0:
+        result.add value"""
+          result[0].add &"proc `==`*(a, b: {cApiName}): bool = uint64(a) == uint64(b)"
+      else:
+        if values.len > 0:
+          result[0].add &"""func toBits*(flags: openArray[{name}]): {cApiName} =
+    for flag in flags:
+      result = {cApiName}(uint(result) or uint(flag))"""
+          result[0].add &"""func toEnums*(number: {cApiName}): seq[{name}] =
+    for value in {name}.items:
+      if (value.ord and cint(number)) > 0:
+        result.add value"""
+          result[0].add &"proc `==`*(a, b: {cApiName}): bool = cint(a) == cint(b)"
+      if predefined_enum_sets.len > 0:
+        result[0].add "const"
+        result[0].add predefined_enum_sets
+      result[0].add "type"
+
+
+func serializeStruct(node: XmlNode): seq[string] =
+  let name = node.attr("name")
+  var union = ""
+  if node.attr("category") == "union":
+    union = "{.union.} "
+  result.add &"  {name}* {union}= object"
+  for member in node.findAll("member"):
+    if not member.hasAttr("api") or member.attr("api") == "vulkan":
+      let fieldname = member.child("name")[0].text.strip(chars={'_'})
+      result.add &"    {mapName(fieldname)}*: {findType(member)}"
+
+func serializeFunctiontypes(api: XmlNode): seq[string] =
+  for node in api.findAll("type"):
+    if node.attr("category") == "funcpointer":
+      let name = node[1][0]
+      let returntype = mapType(node[0].text[8 .. ^1].split('(', 1)[0])
+      var params: seq[string]
+      for i in countup(3, node.len - 1, 2):
+        var paramname = node[i + 1].text.split(',', 1)[0].split(')', 1)[0]
+        var paramtype = node[i][0].text
+        if paramname[0] == '*':
+          paramname = paramname.rsplit(" ", 1)[1]
+          paramtype = "ptr " & paramtype
+        paramname = mapName(paramname)
+        params.add &"{paramname}: {mapType(paramtype)}"
+      let paramsstr = params.join(", ")
+      result.add(&"  {name}* = proc({paramsstr}): {returntype} {{.cdecl.}}")
+
+func serializeConsts(api: XmlNode): seq[string] =
+  result = @["const"]
+  for enums in api.findAll("enums"):
+    if enums.attr("name") == "API Constants":
+      for theenum in enums.findAll("enum"):
+        if theenum.hasAttr("alias"):
+          result.add &"  {theenum.attr(\"name\")}* = {theenum.attr(\"alias\")}"
+        else:
+          var value = theenum.attr("value").strip(chars={'(', ')'})
+          if value.endsWith("U"):
+            value = value[0..^2] & "'u32"
+          elif value.endsWith("ULL"):
+            value = value[0..^4] & "'u64"
+          if value[0] == '~':
+            value = "not " & value[1..^1]
+          result.add &"  {theenum.attr(\"name\")}*: {mapType(theenum.attr(\"type\"))} = {value}"
+
+func serializeType(node: XmlNode, headerTypes: Table[string, string]): Table[string, seq[string]] =
+  if node.attrsLen == 0:
+    return
+  if node.attr("requires") == "vk_platform" or node.attr("category") == "include":
+    return
+  result["basetypes"] = @[]
+  result["enums"] = @[]
+
+  # include-defined types (in platform headers)
+  if node.attr("name") in headerTypes:
+    for platform in PLATFORM_HEADER_MAP[node.attr("requires")]:
+      let platformfile = "platform/" & platform
+      if not result.hasKey(platformfile):
+        result[platformfile] = @[]
+      result[platformfile].add "  " & node.attr("name").strip(chars={'_'}) & " *{.header: \"" & node.attr("requires") & "\".} = object"
+  # generic base types
+  elif node.attr("category") == "basetype":
+    let typechild = node.child("type")
+    let namechild = node.child("name")
+    if typechild != nil and namechild != nil:
+      var typename = typechild[0].text
+      if node[2].kind == xnText and node[2].text.strip() == "*":
+        typename = &"ptr {typename}"
+      result["basetypes"].add &"  {namechild[0].text}* = {mapType(typename)}"
+    elif namechild != nil:
+      result["basetypes"].add &"  {namechild[0].text}* = object"
+  # function pointers need to be handled with structs
+  elif node.attr("category") == "funcpointer":
+    discard
+  # preprocessor defines, ignored
+  elif node.attr("category") == "define":
+    discard
+  # bitmask aliases
+  elif node.attr("category") == "bitmask":
+    if node.hasAttr("alias"):
+      let name = node.attr("name")
+      let alias = node.attr("alias")
+      result["enums"].add &"  {name}* = {alias}"
+  # distinct resource ID types aka handles
+  elif node.attr("category") == "handle":
+    if not node.hasAttr("alias"):
+      let name = node.child("name")[0].text
+      var thetype = mapType(node.child("type")[0].text)
+      result["basetypes"].add &"  {name}* = distinct {thetype}"
+  # enum aliases
+  elif node.attr("category") == "enum":
+    if node.hasAttr("alias"):
+      let name = node.attr("name")
+      let alias = node.attr("alias")
+      result["enums"].add &"  {name}* = {alias}"
+  else:
+    discard
+
+func serializeCommand(node: XmlNode): (string, string) =
+  let
+    proto = node.child("proto")
+    resulttype = mapType(proto.child("type")[0].text)
+    name = proto.child("name")[0].text
+  var params: seq[string]
+  for param in node:
+    if param.tag == "param" and param.attr("api") in ["", "vulkan"]:
+      let fieldname = param.child("name")[0].text.strip(chars={'_'})
+      params.add &"{mapName(fieldname)}: {findType(param)}"
+  let allparams = params.join(", ")
+  return (name, &"proc({allparams}): {resulttype} {{.stdcall.}}")
+
+
+proc update(a: var Table[string, seq[string]], b: Table[string, seq[string]]) =
+  for k, v in b.pairs:
+    if not a.hasKey(k):
+      a[k] = @[]
+    a[k].add v
+
+
+proc main() =
+  let file = getTempDir() / "vk.xml"
+  if not os.fileExists(file):
+    let client = newHttpClient()
+    let glUrl = "https://raw.githubusercontent.com/KhronosGroup/Vulkan-Docs/main/xml/vk.xml"
+    client.downloadFile(glUrl, file)
+
+  let api = loadXml(file)
+
+  const outdir = "src/vulkan_api/output"
+  removeDir outdir
+  createDir outdir
+  createDir outdir / "platform"
+
+  # index all names that are only available on certain platforms
+  var platformTypes: Table[string, string]
+  for extension in api.findAll("extension"):
+    if extension.hasAttr("platform"):
+      for thetype in extension.findAll("type"):
+        platformTypes[thetype.attr("name")] = extension.attr("platform")
+      for command in extension.findAll("command"):
+        platformTypes[command.attr("name")] = extension.attr("platform")
+    elif extension.attr("name").startsWith("VK_KHR_video"):
+      for thetype in extension.findAll("type"):
+        platformTypes[thetype.attr("name")] = "provisional"
+      for command in extension.findAll("command"):
+        platformTypes[command.attr("name")] = "provisional"
+
+  var outputFiles = {
+    "basetypes": @[
+      "import std/dynlib",
+      "import std/tables",
+      "import std/strutils",
+      "import std/logging",
+      "import std/typetraits",
+      "import std/macros",
+      "type",
+      "  VkHandle* = distinct uint",
+      "  VkNonDispatchableHandle* = distinct uint",
+      "when defined(linux):",
+      "  let vulkanLib* = loadLib(\"libvulkan.so.1\")",
+      "when defined(windows):",
+      "  let vulkanLib* = loadLib(\"vulkan-1.dll\")",
+      "if vulkanLib == nil:",
+      "  raise newException(Exception, \"Unable to load vulkan library\")",
+      "func VK_MAKE_API_VERSION*(variant: uint32, major: uint32, minor: uint32, patch: uint32): uint32 {.compileTime.} =",
+      "  (variant shl 29) or (major shl 22) or (minor shl 12) or patch",
+      "",
+      """template checkVkResult*(call: untyped) =
+  when defined(release):
+    discard call
+  else:
+    # yes, a bit cheap, but this is only for nice debug output
+    var callstr = astToStr(call).replace("\n", "")
+    while callstr.find("  ") >= 0:
+      callstr = callstr.replace("  ", " ")
+    debug "CALLING vulkan: ", callstr
+    let value = call
+    if value != VK_SUCCESS:
+      error "Vulkan error: ", astToStr(call), " returned ", $value
+      raise newException(Exception, "Vulkan error: " & astToStr(call) &
+          " returned " & $value)""",
+    """
+# custom enum iteration (for enum values > 2^16)
+macro enumFullRange(a: typed): untyped =
+  newNimNode(nnkBracket).add(a.getType[1][1..^1])
+
+iterator items*[T: HoleyEnum](E: typedesc[T]): T =
+  for a in enumFullRange(E): yield a""",
+    ],
+    "structs": @["type"],
+    "enums": @["type"],
+    "commands": @[],
+  }.toTable
+  outputFiles["basetypes"].add serializeConsts(api)
+  outputFiles["basetypes"].add "type"
+
+  # enums
+  for thetype in api.findAll("type"):
+    if thetype.attr("category") == "bitmask" and not thetype.hasAttr("alias") and (not thetype.hasAttr("api") or thetype.attr("api") == "vulkan"):
+      let name = thetype.child("name")[0].text
+      outputFiles["enums"].add &"  {name}* = distinct VkFlags"
+
+  var bitfields: Table[string, string]
+  outputFiles["enums"].add "let vkGetInstanceProcAddr = cast[proc(instance: VkInstance, name: cstring): pointer {.stdcall.}](checkedSymAddr(vulkanLib, \"vkGetInstanceProcAddr\"))"
+  outputFiles["enums"].add "type"
+  for theenum in api.findAll("enums"):
+    let (enums, bitFieldName) = serializeEnum(theenum, api)
+    outputFiles["enums"].add enums
+    if bitFieldName != "":
+      bitfields[theenum.attr("name")] = bitFieldName
+
+  # bitmask-to-string functions
+  for thetype in api.findAll("type"):
+    if thetype.attr("name") in bitfields:
+      let name = bitfields[thetype.attr("name")]
+      let stringfunc = &"proc `$`*(bitset: {name}): string = $toEnums(bitset)"
+      if not (stringfunc in outputFiles["enums"]):
+        outputFiles["enums"].add stringfunc
+  outputFiles["enums"].add "type"
+
+  # structs and function types need to be in same "type" block to avoid forward-declarations
+  outputFiles["structs"].add serializeFunctiontypes(api)
+  for thetype in api.findAll("type"):
+    if thetype.attr("category") == "struct" or thetype.attr("category") == "union":
+      var outfile = "structs"
+      if thetype.attr("name") in platformTypes:
+        outfile = "platform/" & platformTypes[thetype.attr("name")]
+      if not (outfile in outputFiles):
+        outputFiles[outfile] = @[]
+      outputFiles[outfile].add serializeStruct(thetype)
+
+  # types
+  var headerTypes: Table[string, string]
+  for types in api.findAll("types"):
+    for thetype in types.findAll("type"):
+      if thetype.attrsLen == 2 and thetype.hasAttr("requires") and thetype.hasAttr("name") and thetype.attr("requires") != "vk_platform":
+        let name = thetype.attr("name")
+        let incld = thetype.attr("requires")
+        headerTypes[name] = &"{name} {{.header: \"{incld}\".}} = object"
+
+  for typesgroup in api.findAll("types"):
+    for thetype in typesgroup.findAll("type"):
+      outputFiles.update serializeType(thetype, headerTypes)
+
+  for typesgroup in api.findAll("types"):
+    for node in typesgroup.findAll("type"):
+      if node.attr("category") == "handle":
+        if not node.hasAttr("alias"):
+          let name = node.child("name")[0].text
+          outputFiles["basetypes"].add &"proc `$`*(handle: {name}): string = \"{name}(\" & $(uint(handle)) & \")\""
+          outputFiles["basetypes"].add &"proc valid*(handle: {name}): bool = uint(handle) != 0"
+          outputFiles["basetypes"].add &"proc reset*(handle: var {name}) = handle = {name}(0)"
+          outputFiles["basetypes"].add &"proc `==`*(a, b: {name}): bool = uint(a) == uint(b)"
+          
+
+  # commands aka functions
+  var varDecls: Table[string, string]
+  var procLoads: Table[string, string] # procloads need to be packed into feature/extension loader procs
+  for commands in api.findAll("commands"):
+    for command in commands.findAll("command"):
+      if command.attr("api") != "vulkansc" and not (command.attr("name") in IGNORED_COMMANDS):
+        if command.hasAttr("alias"):
+          let name = command.attr("name")
+          let alias = command.attr("alias")
+          let thetype = varDecls[alias].split(":", 1)[1].strip()
+          varDecls[name] = &"  {name}*: {thetype}"
+          procLoads[name] = &"  {name} = {alias}"
+        else:
+          let (name, thetype) = serializeCommand(command)
+          varDecls[name] = &"  {name}*: {thetype}"
+          procLoads[name] = &"  {name} = cast[{thetype}](vkGetInstanceProcAddr(instance, \"{name}\"))"
+  var declared: seq[string]
+  var featureloads: seq[string]
+  for feature in api.findAll("feature"):
+    if feature.attr("api") in ["vulkan", "vulkan,vulkansc"]:
+      let name = feature.attr("name")
+      outputFiles["commands"].add &"# feature {name}"
+      outputFiles["commands"].add "var"
+      for command in feature.findAll("command"):
+        if not (command.attr("name") in declared) and not (command.attr("name") in IGNORED_COMMANDS):
+          outputFiles["commands"].add varDecls[command.attr("name")]
+          declared.add command.attr("name")
+      featureloads.add &"load{name}"
+      outputFiles["commands"].add &"proc load{name}*(instance: VkInstance) ="
+      for command in feature.findAll("command"):
+        if not (command.attr("name") in IGNORED_COMMANDS & GLOBAL_COMMANDS):
+          outputFiles["commands"].add procLoads[command.attr("name")]
+    outputFiles["commands"].add ""
+  outputFiles["commands"].add ["proc loadVulkan*(instance: VkInstance) ="]
+  for l in featureloads:
+    outputFiles["commands"].add [&"  {l}(instance)"]
+  outputFiles["commands"].add ""
+
+  # for promoted extensions, dependants need to call the load-function of the promoted feature/extension
+  # use table to store promotions
+  var promotions: Table[string, string]
+  for extensions in api.findAll("extensions"):
+    for extension in extensions.findAll("extension"):
+      if extension.hasAttr("promotedto"):
+        promotions[extension.attr("name")] = extension.attr("promotedto")
+
+  var extensionDependencies: Table[string, (seq[string], XmlNode)]
+  var features: seq[string]
+  for feature in api.findAll("feature"):
+    features.add feature.attr("name")
+  for extensions in api.findAll("extensions"):
+    for extension in extensions.findAll("extension"):
+      let name = extension.attr("name")
+      extensionDependencies[name] = (@[], extension)
+      if extension.hasAttr("depends"):
+        extensionDependencies[name] = (extension.attr("depends").split("+"), extension)
+        if extension.attr("depends").startsWith("("): # no need for full tree parser, only single place where we can use a feature
+          let dependencies = extension.attr("depends").rsplit({')'}, 1)[1][1 .. ^1].split("+")
+          extensionDependencies[name] = (dependencies, extension)
+      if name in SPECIAL_DEPENDENCIES:
+        extensionDependencies[name][0].add SPECIAL_DEPENDENCIES[name]
+
+  # order dependencies to generate them in correct order
+  var dependencyOrderedExtensions: OrderedTable[string, XmlNode]
+  while extensionDependencies.len > 0:
+    var delkeys: seq[string]
+    for extensionName, (dependencies, extension) in extensionDependencies.pairs:
+      var missingExtension = false
+      for dep in dependencies:
+        let realdep = promotions.getOrDefault(dep, dep)
+        if not (realdep in dependencyOrderedExtensions) and not (realdep in features):
+          missingExtension = true
+          break
+      if not missingExtension:
+        dependencyOrderedExtensions[extensionName] = extension
+        delkeys.add extensionName
+    for key in delkeys:
+      extensionDependencies.del key
+
+  var extensionLoaderMap: Table[string, Table[string, string]]
+  for extension in dependencyOrderedExtensions.values:
+    if extension.hasAttr("promotedto"): # will be loaded in promoted place
+      continue
+    if extension.attr("supported") in ["", "vulkan", "vulkan,vulkansc"]:
+      var file = "commands"
+      var platform = extension.attr("platform")
+      if extension.attr("name").startsWith("VK_KHR_video"):
+        platform = "provisional"
+      if platform != "":
+        file = "platform/" & platform
+      let name = extension.attr("name")
+      if extension.findAll("command").len > 0:
+        outputFiles[file].add &"# extension {name}"
+        outputFiles[file].add "var"
+        for command in extension.findAll("command"):
+          if not (command.attr("name") in declared) and not (command.attr("name") in IGNORED_COMMANDS):
+            outputFiles[file].add varDecls[command.attr("name")]
+            declared.add command.attr("name")
+      outputFiles[file].add &"proc load{name}*(instance: VkInstance) ="
+      if not (platform in extensionLoaderMap):
+        extensionLoaderMap[platform] = Table[string, string]()
+      extensionLoaderMap[platform][name] = &"load{name}"
+      var addedFunctionBody = false
+      if extension.hasAttr("depends"):
+        for dependency in extension.attr("depends").split("+"):
+          # need to check since some extensions have no commands and therefore no load-function
+          outputFiles[file].add &"  load{promotions.getOrDefault(dependency, dependency)}(instance)"
+          addedFunctionBody = true
+      for command in extension.findAll("command"):
+        outputFiles[file].add procLoads[command.attr("name")]
+        addedFunctionBody = true
+      if not addedFunctionBody:
+        outputFiles[file].add "  discard"
+      outputFiles[file].add ""
+
+  var mainout: seq[string]
+  for section in ["basetypes", "enums", "structs", "commands"]:
+    mainout.add outputFiles[section]
+  mainout.add "var EXTENSION_LOADERS = {"
+  for extension, loader in extensionLoaderMap[""].pairs:
+    mainout.add &"  \"{extension}\": {loader},"
+  mainout.add "}.toTable"
+  for platform in api.findAll("platform"):
+    mainout.add &"when defined({platform.attr(\"protect\")}):"
+    mainout.add &"  ../vulkan/include platform/{platform.attr(\"name\")}"
+    if platform.attr("name") in extensionLoaderMap:
+      for extension, loader in extensionLoaderMap[platform.attr("name")].pairs:
+        mainout.add &"  EXTENSION_LOADERS[\"{extension}\"] = {loader}"
+
+  mainout.add ""
+  mainout.add "proc loadExtension*(instance: VkInstance, extension: string) = EXTENSION_LOADERS[extension](instance)"
+  mainout.add ""
+  mainout.add "# load global functions immediately"
+  mainout.add "block globalFunctions:"
+  mainout.add "  let instance = VkInstance(0)"
+  for l in GLOBAL_COMMANDS:
+    mainout.add procLoads[l]
+  mainout.add ""
+  # produces error if enable both implicit converters
+  # mainout.add "converter VkBool2NimBool*(a: VkBool32): bool = a > 0"
+  mainout.add "converter NimBool2VkBool*(a: bool): VkBool32 = VkBool32(a)"
+
+  writeFile outdir / &"api.nim", mainout.join("\n")
+
+
+  for filename, filecontent in outputFiles.pairs:
+    if filename.startsWith("platform/"):
+      writeFile outdir / &"{filename}.nim", (@[
+      "type"
+      ] & filecontent).join("\n")
+
+when isMainModule:
+  main()