blog:2022:1104_vulkanapp_initial_triangle_display

# NervLuna: Continuing with VulkanApp restoring

Still restoring stuff that was already “sort of” working at some point (a long time ago) lol: so I continue with this VulkanApp I built in Lua with my binding system. And it seems there is still a lot to do on that path…

• AS mentioned at the end of the previous post, I currently have an issue with my enums: whic seem to be considered as non-defined classes:
2022-10-04 19:47:10.908671 [DEBUG] Creating enum ::VkPresentModeKHR

2022-10-04 19:47:10.909671 [DEBUG] Adding type with class target name: VkPresentModeKHR, (canonical type: VkPresentModeKHR)

2022-10-04 19:47:10.910393 [DEBUG] TypeManager: Registering type {

"VkPresentModeKHR"

}

and then:

2022-10-04 19:47:16.318061 [DEBUG] Resolving target for type: VkPresentModeKHR

2022-10-04 19:47:16.318345 [DEBUG] Couldn't resolve type target, using target name instead: VkPresentModeKHR

2022-10-04 19:47:16.318518 [DEBUG] Creating class ::VkPresentModeKHR
• ⇒ In fact the “type” for VkPresentModeKHR should really be registerd as an integer I think, let's fix that…
• Fixed! But this was a bit tricky in the end: I had to introduce a new “ENUM” basic type for my types as using only int32_t for all enums would not compile (where we need the enum itself and not just an int value!)
• One nasty issue I just noticed is that with the latest changed I introduced replacing “typedefs” with the “using” syntax, I now have an unhandled curcor type case, for instance:
2022-10-06 15:09:27.010095 [DEBUG] Cursor 'VkDeviceQueueCreateInfoList' of kind 'TypeAliasDecl'
• So I need to deal with this TypeAliasDecl type now.
• ⇒ turned out this fix was an easy one: all I needed was to handle this TypeAliasDecl just as regular typedefs:
    elseif ckind == clang.CursorKind.TypedefDecl or ckind == clang.CursorKind.TypeAliasDecl then
-- logDEBUG("(Namespace) Registering typedef for '",cname, "'")
self:registerTypedef(cur)
else
logDEBUG("Cursor '",cname, "' of kind '", kindName,"'")
end

• Another issue I just noticed now is that if I add class declarations somewhere like:
namespace nvk {

class VulkanPhysicalDevice;
class VulkanInstance;
class VulkanSurface;

class NVVULKAN_EXPORT VulkanDevice : public nv::RefObject {
NV_DECLARE_NO_COPY(VulkanDevice)
NV_DECLARE_NO_MOVE(VulkanDevice)

};

• Then I actually just get a placeholder for the classes VulkanSurface, etc… not good 🤕
• ⇒ arrff, and this was due to usage of logDEBUG2() in of of the curvor visitor function… grrrr… I should really make those visitor fail harder.
• ⇒ Cool! Now that I got this fixed, the next step is the createSwapChain function.
• Before I move to the swapchain creation there is another nice feature I think I should investigate a bit: and that's the automatic creation of a Vector from an input lua table. How could we do that ?
• Let's consider for instance the VulkanInstance::create_device() method:
auto
create_device(const nv::StringList& extensions,
const QueueInfoList& queueInfos,
const VkPhysicalDeviceFeatures* desired_features = nullptr,
uint32_t devId = 0) -> nv::RefPtr<VulkanDevice>
• This should accept a StringList, and a QueueInfoList as arguments
• ⇒ What this means is that for Vector classes that are passed as const references, we could also accept lua tables on the stack, something like this:
static auto _check_create_device_sig1(lua_State* L) -> bool {
int luatop = lua_gettop(L);
if( luatop<3 || luatop>5 ) return false;

if( !luna_isInstanceOf(L,1,LUNA_SID("nvk::VulkanInstance")) ) return false;

if( !luna_isInstanceOf(L,2,LUNA_SID("nv::StringList"),false) && lua_istable(L,2)!=1) return false;
if( !luna_isInstanceOf(L,3,LUNA_SID("nvk::QueueInfoList"),false) && lua_istable(L,2)!=1 ) return false;
if( luatop>=4 && !luna_isInstanceOf(L,4,LUNA_SID("VkPhysicalDeviceFeatures"),true) ) return false;
if( luatop>=5 && lua_isnumber(L,5)!=1 ) return false;
return true;
}
• Then when we have to retrieve the argument from the stack we need a bit of work too on the following code:
static auto _bind_create_device_sig1(lua_State* L) -> int {
int luatop = lua_gettop(L);
nvk::VulkanInstance* self = Luna< nvk::VulkanInstance >::get(L,1);
LUNA_ASSERT(self!=nullptr);

nv::StringList* extensions = Luna< nv::StringList >::get(L,2,false);
nvk::QueueInfoList* queueInfos = Luna< nvk::QueueInfoList >::get(L,3,false);
VkPhysicalDeviceFeatures* desired_features = luatop>=4? Luna< VkPhysicalDeviceFeatures >::get(L,4,true) : nullptr;
uint32_t devId = luatop>=5? (uint32_t)lua_tointeger(L,5) : (uint32_t)0;

nv::RefPtr<nvk::VulkanDevice> res = self->create_device(*extensions, *queueInfos, luatop>=4 ? desired_features : nullptr, devId);
Luna< nvk::VulkanDevice >::push(L, res.get(), true);

return 1;
}
• In fact we need so much refactoring here that I'm not sure it's worth it in the end 🤔… We need some storage for the StringList we may have to create for instance if we have a table ?… or maybe we could tweak the get() function directly ?? Nnnaaayy: that's sounds too tricky.
• So let's say we put an object on the stack, then we could use something like this:
nv::StringList extensions_list;
nv::StringList* extensions = &extensions_list;
if(lua_istable(L, 2)==1) {
// Populate the extensions_list object here:
num = (int)lua_objlen(L, 2);
for (int i = 0; i < num; ++i) {
lua_rawgeti(L, 2, i + 1);
nv::String str = // Read the data from stack here as a nv::String value.
extensions_list.push_back(str);
lua_pop(L, 1);
}
}
else {
// Otherwise we must already have a valid StringList object:
extensions = Luna< nv::StringList >::get(L,2,false);
}


• Either this, or I just extend the XXXList constructors in lua to accept an in put table… ? That would still require creating a Vector on the lua Stack, doesn't soon very good.
• And in fact an “extension” of the code above could be to “require” the address of a given object when we need if from a shared memory pool, and then release it when we don't need it anymore at the end of the function call ⇒ this should avoid the bulk of dynamic allocations ? Or maybe just placing the list on the stack is really enough here in fact.
• So let's see how we could implement something like that now 😇
• Oki doki!: in the end this was not as tricky as I thought it would be, and here is a example of the code generated now:
static auto _bind_create_device_sig1(lua_State* L) -> int {
int luatop = lua_gettop(L);
nvk::VulkanInstance* self = Luna< nvk::VulkanInstance >::get(L,1);
LUNA_ASSERT(self!=nullptr);

nv::StringList extensions_vec;
nv::StringList* extensions = &extensions_vec;
if(lua_istable(L, 2)==1) {
int num = (int)lua_objlen(L, 2);
for (int i = 0; i < num; ++i) {
lua_rawgeti(L, 2, i + 1);
size_t elem_len = 0;
const char* elem_cstr = lua_tolstring(L,-1,&elem_len);
nv::String elem(elem_cstr, elem_len);
extensions_vec.push_back(std::move(elem));
lua_pop(L, 1);
}
}
else {
extensions = Luna< nv::StringList >::get(L,2,false);
}
nvk::QueueInfoList queueInfos_vec;
nvk::QueueInfoList* queueInfos = &queueInfos_vec;
if(lua_istable(L, 3)==1) {
int num = (int)lua_objlen(L, 3);
for (int i = 0; i < num; ++i) {
lua_rawgeti(L, 3, i + 1);
nvk::QueueInfo* elem = Luna< nvk::QueueInfo >::get(L,-1,false);
queueInfos_vec.push_back(*elem);
lua_pop(L, 1);
}
}
else {
queueInfos = Luna< nvk::QueueInfoList >::get(L,3,false);
}
VkPhysicalDeviceFeatures* desired_features = luatop>=4? Luna< VkPhysicalDeviceFeatures >::get(L,4,true) : nullptr;
uint32_t devId = luatop>=5? (uint32_t)lua_tointeger(L,5) : (uint32_t)0;

nv::RefPtr<nvk::VulkanDevice> res = self->create_device(*extensions, *queueInfos, luatop>=4 ? desired_features : nullptr, devId);
Luna< nvk::VulkanDevice >::push(L, res.get(), true);

return 1;
}
• This is a bit verbose, but it seems to be working just fine, and I could then update the lua code accordingly:
    -- Now we create a device with a simple queue:
local qflags = VkQueueFlagBits
-- local qinfos = nvk.QueueInfoList()
-- qinfos:push_back()
-- local extNames = nv.StringList()
-- extNames:push_back()

local dev = inst:create_device({exts.VK_KHR_SWAPCHAIN}, {nvk.QueueInfo(qflags.GRAPHICS_BIT, 0.3)}, nil, 0)
logDEBUG("Done creating device.")
• Hmmm 🤔, and now, thinking about it, maybe we can also get the vector initialization from lua almost for free just defining an additional constructor. Okay: so I simply injected a new copy constructor for the vector types, and this seems to do the trick:
  -- Inject the functions:
create_func(cl:getName(), "void ()")
create_func(cl:getName(), "void (const "..fname.."&)", nil, {{"rhs", cref_type}})
• In addition to the luna extension mechanism I already have which is handy to add specific extension functions, I now realize it would also be great to be able to provided some more generic lua functions, for instance in the case of the Vector containers:
• I could certainly use for instance a “forEach()” or “to_table()” method, which I could bind as usual, but to bind these, the actual method should also exist…
• So, implementing the for_each() lua function for Vectors went fine: this is done directly in the ClassWriter with the following code:
  -- Also write our custom functions here directly:
self:writeSubLine([[
// Here I'm writing the custom functions that will be bound below for vector class ${1} // with elements of type${2}

static void _lunaext_for_each(const ${1}& obj, lua_State* L, LuaFunction& func) { for(auto i=0; i<obj.size(); ++i) { func((lua_Integer)i, obj[i]); } }; ]], fname, el_name) • Yet in this process I was also restoring the LuaFunction helper class, and thus tried to improve a bit on how we call that function with variadic templates to pass the arguments… except that, this doesn't quite work 😅: I'mm not quite sure why but this version because will lead to a crash when running the app:  template <typename T> void pushArgs(const T& arg0) { logDEBUG("Pushing single arg."); pushArg(arg0); } template <typename T, typename... Args> auto pushArgs(T arg0, const Args&... args) -> bool { logDEBUG("Pushing 1 arg."); pushArg(arg0); logDEBUG("Pushing more args..."); pushArgs(args...); logDEBUG("Done pushing args."); }  • ⇒ But never mind: I lost enough time on this for now, so I'm using this version instead (which is working fine):  template <typename T> void pushArgs(const T& arg0) { pushArg(arg0); } template <typename T1, typename T2> void pushArgs(const T1& arg1, const T2& arg2) { pushArg(arg1); pushArg(arg2); } template <typename T1, typename T2, typename T3> void pushArgs(const T1& arg1, const T2& arg2, const T3& arg3) { pushArg(arg1); pushArg(arg2); pushArg(arg3); } template <typename T1, typename T2, typename T3, typename T4> void pushArgs(const T1& arg1, const T2& arg2, const T3& arg3, const T4& arg4) { pushArg(arg1); pushArg(arg2); pushArg(arg3); pushArg(arg4); }  • Our custom luna extensions wit lunaext and lunarawext already give us a lot of flexibility to add new functions on a given class, yet this is only really working with already existing objects as we concluded in our previous post. For constructors we will need something different, let's see… • OK ⇒ Just added support for custom lua constructors which can be implemented as follow: inline auto _lunactr_VkExtent2D(nv::U32 width = 0, nv::U32 height = 0) -> VkExtent2D* { // logDEBUG("Should create a new extent2D here with witdh={}, height={}", // width, height); return new VkExtent2D{width, height}; } • Adding more Vulkan classes and more bindings, I eventually got my minimal test application to run 😃! Well, just displaying a white screen with no actual vulkan drawing on it, but that's a start anyway: • What I noticed next was that I was getting a crash when deleting my VkSurfaceFormatList object in lua during the application uninit process: this was because there was valid destructor available in those bindings at first, but this was producing a C++ exception while I really expeced a lua error in the LUNA_THROW_MSG: ⇒ So this required some fixing, • And at the same time, I noticed that the message handlers in luna (do display debug/error outputs) were not connected yet, so now also connecting them in nerv_bindings.cpp:  luna::setLogFunctions(luna_debug_log, luna_error_log); // Also prepare here the registration of the void type: luna::pushGlobalNamespace(L); luna::Luna<void>::Register(L); luna::popNamespace(L); • Ohh, and also, as shown above, I get vulkan validation error messages which is great 👍! So, what's our next step now ? Let's think a little with a small coffee in hand ☕… (that's my favorite part 🙃) • Unfortunately, it seems this is where I stopped in my previous iteration on vulkan, and then I moved to DirectX12 where I pushed it a little further… So now I need to find some resources on performing simple display with vulkan and work from there. • ⇒ Okay, so I think what I will need next is to build a graphics pipeline actually, let's get started on this! • Added even more vulkan classes to support creation of Pipelines: OK • Next let's create a first shader file: #version 450 #ifdef __VERTEX__ vec2 positions[3] = vec2[] { vec2(0.0, -0.5), vec2(0.5, 0.5), vec2(-0.5, 0.5) }; void main() { gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0); } #endif #ifdef __FRAGMENT__ layout(location=0) out vec4 outColor; void main() { outColor = vec4(1.0, 0.0, 0.0, 1.0); } #endif  • ⇒ Okay, so I think I really need to try and embed that glslang library now, so let's do this. • One problem there: glslang::DefaultTBuiltInResource is not available anymore. ⇒ replacement solution found at https://github.com/KhronosGroup/glslang/issues/2207 • OK, now I have this method ready for usage in VulkanDevice:  /* Helper method used to compile GLSL code to SPIRV */ auto compile_glsl_to_spirv(ShaderStage stage, const char* shader_code, const char* code_name) -> nv::String; • Now I'm trying to compile that GLSL code from Lua with the following snippet:  logDEBUG("Loading Vulkan bindings...") bm:loadBindings("Vulkan", "luaVulkan") -- Create the Library: local lib = nvk.VulkanLibrary() local app = nv.NervApp.instance() app:register_component(lib) lib:init() -- Here we should be able to load a GLSL source file: local code = lib:get_glsl_source("tests/test_simple.glsl") logDEBUG("Loaded code is: ", code) -- Now we request the compilation: vert_code = "#define __VERTEX__\n"..code frag_code = "#define __FRAGMENT__\n"..code vert_spirv = lib:compile_glsl_to_spirv(nvk.ShaderStage.VERTEX, vert_code, "test_simple_vert") logDEBUG("Compiled vertex shader spirv code length: ", #vert_spirv) frag_spirv = lib:compile_glsl_to_spirv(nvk.ShaderStage.FRAGMENT, frag_code, "test_simple_frag") logDEBUG("Compiled fragment shader spirv code length: ", #frag_spirv) • The compilation process will give me the following results: 2022-10-14 08:15:33.371281 [ERROR] GLSL preprocessing failed in test_simple_vert: 2022-10-14 08:15:33.371312 [ERROR] Info log: WARNING: 0:1: '#define' : names containing consecutive underscores are reserved: __VERTEX__ ERROR: 0:2: '#version' : must occur first in shader ERROR: 0:2: '#version' : bad profile name; use es, core, or compatibility ERROR: 0:2: '#version' : bad tokens following profile -- expected newline ERROR: 3 compilation errors. No code generated. 2022-10-14 08:15:33.371322 [ERROR] Debug log: 2022-10-14 08:15:33.371375 [DEBUG] Compiled vertex shader spirv code length: 0 2022-10-14 08:15:33.372532 [ERROR] GLSL preprocessing failed in test_simple_frag: 2022-10-14 08:15:33.372544 [ERROR] Info log: WARNING: 0:1: '#define' : names containing consecutive underscores are reserved: __FRAGMENT__ ERROR: 0:2: '#version' : must occur first in shader ERROR: 0:2: '#version' : bad profile name; use es, core, or compatibility ERROR: 0:2: '#version' : bad tokens following profile -- expected newline ERROR: 3 compilation errors. No code generated. 2022-10-14 08:15:33.372554 [ERROR] Debug log: 2022-10-14 08:15:33.372577 [DEBUG] Compiled fragment shader spirv code length: 0 • ⇒ So that's interesting: it just means that I should keep the #version out of the glsl code file themself and add that dynamically. and also, I should not use double underscores in my shader borders as reported above. • After some more tweaking I got the fragment shader to compile and for the vertex shader I get the error: 2022-10-14 08:27:22.758819 [ERROR] GLSL parsing failed in test_simple_vert: 2022-10-14 08:27:22.758845 [ERROR] Info log: ERROR: 0:13: 'limitations' : Non-constant-index-expression ERROR: 1 compilation errors. No code generated. • Which seems to be related to the line:  gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0); • Also trying to replacegl_Vertexindex with gl_VertexID but this is not accepted (so “Vulkan GLSL” is not the same as “OpenGL GLSL” 🤔 ?) • So what do we have here ? • Hmmm, now I'm thinking maybe I have a mix here between the vulkan provided libraries for glslang and the standalone package I've been trying to use… • ⇒ And indeed this was it! If I comment the line # link_directories(${VULKAN_DIR}/lib) in my cmake configs, then everything is still compiling just fine (⇒ we are not linking to any vulkan library actually 😅) and I will link to the glslang library version 11.11.0 that I'm providing separately, and the vertex code will now compile!
⇒ The TBuiltInResource structure is different between glslang 11.11.0 and the version provided with vulkan-sdk 1.3.224 so that would explain the shader compilation error.
• With our newly compiled SPIRV code we should now be able to create Shader modules to be used for our pipeline constructions, so let's try that.
• Hmmm, initial shader creation test give me an error as follow:
SPIR-V module not valid: Invalid SPIR-V binary version 1.6 for target environment SPIR-V 1.0 (under Vulkan 1.0 semantics)
• So, where on earth would I set this API version ? 😅
• OK Found it at the creation of the vulkan instance, and updated to apiVersion 1.3 and now the error is gone:
    // We prepare the application info:
VkApplicationInfo application_info = {VK_STRUCTURE_TYPE_APPLICATION_INFO,
nullptr,
lib->get_application_name().c_str(),
VK_MAKE_VERSION(1, 0, 0),
lib->get_engine_name().c_str(),
VK_MAKE_VERSION(1, 0, 0),
VK_MAKE_VERSION(1, 3, 0)};
• Next we will create the command buffers needed to produce the final images: we will provide one command buffer per image available in the swapchain:
function Class:createCommandBuffers()
local queue = self.vkdev:get_queue(0)
local famIdx = queue:get_queue_family_index()
logDEBUG("Queue 0 family index: ", famIdx)

-- Now we create a Command Pool on that family index:
self.cmdpool = self.vkdev:create_command_pool(famIdx, VkCommandPoolCreateFlagBits.RESET_COMMAND_BUFFER_BIT);
logDEBUG("Created command pool");

-- Then we create a command buffer:
-- We create as many buffers as we have images in our swapchain
local num = self.swapchain:get_num_images()
self.cmdbufs = self.cmdpool:create_command_buffers(num, VkCommandBufferLevel.PRIMARY);
logDEBUG("Created command buffers");
end
• And before I go any further, I think I should really stop the implementation here and first move all those vulkan classes/enums/functions into a dedicated lua namespace/table instead of placing everything in the global table. Lets see how to do that…
• To use a default namespace we can simply specify that in the nervbind.lua config:
cfg.defaultNamespaceName = "vk"
• To provide a custom “lua alias” for some classes and/or enums, we can set the alias while iterating on all the discovered classes:
local preprocessEnums = function(ent)

local name = ent:getFullName()

-- rename vulkan enums:
if name:startsWith("Vk") then
ent:setLuaAlias(name:sub(3))
end

for _,p in ipairs(allowedEnums) do
if name:find(p) then
return
end
end

-- Ignore this entity:
ent:setIgnored(true)
-- logDEBUG("=> Ignoring entity ", name)
return

end
⇒ I just discovered https://github.com/sumneko/lua-language-server 🤣 My god, this will save me so much time and effort…
• My my… this lua-language-server thing is amasing: and now this makes me realize I could automatically generate “libraries” for it when I generate my bindings, which ultimately means I could get auto-completion and diagnostic supports for the C++ elements bound to lua 😳! Really ?! That would be so terrible good… 🥳 ⇒ Let's see if I could start with providing enum values for instance:
• Currently using the following settings for lls in vscode:
  "Lua.diagnostics.globals": [
"vk",
"nvk",
"nv",
"clang",
"nerv",
"lfs",
"nvLogTRACE",
"nvLogDEBUG",
"nvLogWARN",
"nvLogERROR",
"nvLogFATAL",
"nvCHECK",
"nvTHROW"
],
"Lua.runtime.version": "LuaJIT",
// "Lua.workspace.library": ["\${workspaceFolder}/dist/assets/lua/external"],
"Lua.workspace.library": ["dist/assets/lua_defs"],
"Lua.workspace.ignoreDir": [
".cache",
".vscode",
"cmake",
"sources",
"temp",
"tests",
"dist/modules",
"dist/assets/lua_defs",
"dist/assets/lua/external",
"dist/assets/lua/pl"
],
"Lua.workspace.checkThirdParty": false
• ⇒ I'm now trying to generate the enum metadata in dist/assets/lua_defs/vulkan.lua
• Yeeahh! That's working pretty well so far: I added support to write the enum definitions as follow for instance:
---@enum vk.MemoryAllocateFlagBits
vk.MemoryAllocateFlagBits =
{
FLAG_BITS_MAX_ENUM = 6,
}
• And then also the support to write the class field definitions as follow:
---@class vk.SurfaceCapabilitiesKHR
---@field minImageCount integer
---@field maxImageCount integer
---@field currentExtent vk.Extent2D
---@field minImageExtent vk.Extent2D
---@field maxImageExtent vk.Extent2D
---@field maxImageArrayLayers integer
---@field supportedTransforms integer
---@field currentTransform vk.SurfaceTransformFlagBitsKHR
---@field supportedCompositeAlpha integer
---@field supportedUsageFlags integer
vk.SurfaceCapabilitiesKHR = {}
The actual enum values that I'm providing as just dummy values to ensure the keys will be sorted in the same order as in the actual bindings: that's definitely something I need to keep in mind to avoid mixing those integer values at some point.
• Next stop: adding support for the functions in each class 👍!
• Whaaoo, that was really a nice implementation experience: everything went really smooth for once, and now I'm able to automatically generate very large libraries definitions for the lua-language-server along with my NervLuna bindings! In the process I'm handling the various requirements to find the correct class names/namespaces in lua, taking care of RefPtr object, string convertions, function overloads, enum resolutions, etc and it seems that LLS is pretty happy with it 😊!
• Here is a speaking example of the type of code generated:
---@class nvk.VulkanSubpassDescription
---@field objects nvk.VkSubpassDescriptionList
---@field pInputAttachments nv.Vector_nvk_VkAttachmentReferenceList
---@field pColorAttachments nv.Vector_nvk_VkAttachmentReferenceList
---@field pResolveAttachments nv.Vector_nvk_VkAttachmentReferenceList
---@field pDepthStencilAttachment nvk.VkAttachmentReferenceList
---@field pPreserveAttachments nv.Vector_nv_U32List
local class = {}
---@param rhs nvk.VulkanSubpassDescription
---@return nvk.VulkanSubpassDescription
function class:op_assign(rhs) end
---@return nvk.VkSubpassDescriptionList
function class:getVkList() end
---@return vk.SubpassDescription
function class:getVk() end
---@return boolean
function class:empty() end
---@return integer
function class:size() end
---@param item vk.SubpassDescription
---@return nvk.VulkanSubpassDescription
---@return vk.SubpassDescription
function class:getCurrent() end
---@param flags_ integer
---@return nvk.VulkanSubpassDescription
function class:setFlags(flags_) end
---@param pipelineBindPoint_ vk.PipelineBindPoint
---@return nvk.VulkanSubpassDescription
function class:setBindPoint(pipelineBindPoint_) end
---@return nvk.VkAttachmentReferenceList
function class:getCurrentInputAttachmentList() end
---@param list nvk.VkAttachmentReferenceList
---@return nvk.VulkanSubpassDescription
function class:setInputAttachments(list) end
---@param obj vk.AttachmentReference
---@return nvk.VulkanSubpassDescription
---@overload fun(attachment: integer, layout: vk.ImageLayout): nvk.VulkanSubpassDescription
---@return vk.AttachmentReference
---@return nvk.VulkanSubpassDescription
function class:syncInputAttachments() end
---@return nvk.VkAttachmentReferenceList
function class:getCurrentColorAttachmentList() end
---@param list nvk.VkAttachmentReferenceList
---@return nvk.VulkanSubpassDescription
function class:setColorAttachments(list) end
---@param obj vk.AttachmentReference
---@return nvk.VulkanSubpassDescription
---@overload fun(attachment: integer, layout: vk.ImageLayout): nvk.VulkanSubpassDescription
---@return vk.AttachmentReference
---@return nvk.VulkanSubpassDescription
function class:syncColorAttachments() end
---@return nvk.VkAttachmentReferenceList
function class:getCurrentResolveAttachmentList() end
---@param list nvk.VkAttachmentReferenceList
---@return nvk.VulkanSubpassDescription
function class:setResolveAttachments(list) end
---@param obj vk.AttachmentReference
---@return nvk.VulkanSubpassDescription
---@overload fun(attachment: integer, layout: vk.ImageLayout): nvk.VulkanSubpassDescription
---@return vk.AttachmentReference
---@return nvk.VulkanSubpassDescription
function class:syncResolveAttachments() end
---@return vk.AttachmentReference
function class:getCurrentDepthStencilAttachment() end
---@return nvk.VulkanSubpassDescription
---@overload fun(attachment: integer, layout: vk.ImageLayout): nvk.VulkanSubpassDescription
function class:setDepthStencilAttachment() end
---@return boolean
function class:hasDepthStencilAttachment() end
---@return vk.AttachmentReference
function class:getOrCreateDepthStencilAttachment() end
---@return nv.U32List
function class:getCurrentPreserveAttachmentList() end
---@param list nv.U32List
---@return nvk.VulkanSubpassDescription
function class:setPreserveAttachments(list) end
---@param obj integer
---@return nvk.VulkanSubpassDescription

• Function descriptions are still missing, but anyway I'm fully satisfied with this first iteration already 👍! So let's get back to our command buffers now
• One thing that is still annoying in by library construction is that I don't have proper support for constructors yet: for instance when I call:
local lib = nvk.VulkanLibrary()
• ⇒ The lib object is not recognized as a VulkanLibrary instance: so I need to setup the constructor overloads when creating the library to provide this feature.
• OK: now fixed with this additional code:
        -- Get the class constructor:
local ctr = class:getConstructor()
if ctr ~= nil then
local sigs = ctr:getPublicSignatures(true)
for _, sig in ipairs(sigs) do
local args = sig:getArguments()
local anames = {}

for _, arg in ipairs(args) do
-- Ignore the canonical lua state:
if not arg:isCanonicalLuaState() then
local aname = arg:getName()
local atype = arg:getType():getLuaTypeName()
if atype == "luna.LuaFunction" then
atype = "function"
end

table.insert(anames, aname .. ": " .. atype)
end
end

-- Finish the list of arguments:
local elems = {}
table.insert(elems, table.concat(anames, ", "))
table.insert(elems, "): ")

-- The return type should be the class itself:
table.insert(elems, clname)

-- Write that constructor:
self:writeLine(table.concat(elems, ""))
end
end
• Next thing we need is to support tables where we have vector arguments…. Ohh… actually, it seems this is available out of the box and I have nothing to do for it, cool 😆!
• Okay, so I'm getting tired of seeing my computer almost freeze to death when I try to build the C++ sources in this project: time to put the number of threads used under control!
• So I'm adding this section in the cmake_manager.py component:
        nthreads = self.get_param("num_threads", None)
flags = None

builder.run_ninja(build_dir, outfile=outfile, flags=flags)
• And next I need an optional entry to specify the number of threads:
    psr = context.build_parser("build")
psr.add_str("-c", "--compiler", dest="compiler_type", default="clang")("Select the compiler")

• ⇒ And now building with 4 threads on Saturn seems to still be acceptable, all good 👍!
• While I'm at it (trying to optimize the build time) I should also probably try to configure the project to support building in debug mode… Let's see if this is possible 🤔…
• So I added another parameter in the cmake_manager to specify the build type:
        build_type = self.get_param("build_type", "Release")
logger.info("Cmake build type: %s", build_type)

builder = self.get_builder()
builder.run_cmake(build_dir, install_dir, src_dir, flags, outfile=outfile, build_type=build_type)

• Then I can use a build command such as:
nvp nvl_build -j 4 -t Debug
• Yet, of course, I have a problem with linking the boost debug libraries (since I didn't build them I think):
lld-link: error: could not open 'libboost_filesystem-clang14-mt-gd-x64-1_79.lib': no such file or directory
lld-link: error: /failifmismatch: mismatch detected for '_ITERATOR_DEBUG_LEVEL':
>>> sources/nvCore/shared/CMakeFiles/nvCore.dir/_/src/view/WindowManager.cpp.obj has value 2
• ⇒ So now also rebuild boost with Debug version support:
cli.bat build libs boost --rebuild
• Note: I first disabled the variant=release setting in the builder for boost.
• OK, then also fixing the variant of msvc runtime and this was then compiling fine in Debug:
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDLL")
• But the compile times are not significantly better… is there something else I could do ?? ⇒ Nothing I see for now, so never mind.
• Okay so… (a few days later…): I've actually been working pretty hard on this vulkan engine lately: basically adding a VulkanRenderer class which is currently the main element used to render frames. I used some of the design from the LittleVulkanEngine project as reference but then started to deviate from it.
• But anyway, I could then fill the command buffers, and register them on the renderer as follow:
-- Method used to record command buffers:
---@param rpassIdx integer The render pass to use.
function Class:recordCommandBuffers(rpassIdx)
-- Should create the framebuffers for each of the swapchain images here:
-- Before we can create a framebuffer, we need to create the depth image that we will use as attachment:
self.depthImg = self.vkeng:create_image_2d(self.width, self.height, vk.Format.D32_SFLOAT,
vk.ImageUsageFlagBits.DEPTH_STENCIL_ATTACHMENT_BIT)
-- logDEBUG("Depth image idx: ", self.depthImg)
self:allocate_image_memory(self.depthImg)

local depthView = self.vkeng:create_image_view(self.depthImg)
local schain = self.vkeng:get_swap_chain(self.swapchain)

-- Iterate on each command buffer object:
local num = self.cmdbufs:size()
self.framebuffers = nv.U64List()

for i = 0, num - 1 do

-- Create the framebuffer that will be used to render into the image (i):
local imgIdx = schain:get_image(i)
-- self:allocate_image_memory(imgIdx)
local viewIdx = self.vkeng:create_image_view(imgIdx)
local fbIdx = self.vkeng:create_framebuffer(rpassIdx, { viewIdx, depthView }, self.width, self.height)
self.framebuffers:push_back(fbIdx)

local cbuf = self.vkeng:get_command_buffer(self.cmdbufs:at(i))

-- cbuf:begin(vk.CommandBufferUsageFlagBits.ONE_TIME_SUBMIT_BIT)
cbuf:begin(0)

-- Begin rendering into the swapchain framebuffer:
cbuf:begin_inline_pass(rpassIdx, fbIdx)

-- End the render pass:
cbuf:end_render_pass()

-- Finish the command buffer:
cbuf:finish()

end

logDEBUG("Done recording ", num, " command buffers.")
end
• Then extended my VulkanApp run loop to also render a frame with the renderer on each cycle, and here… ohhh miracle! It's working! And I get a black screen! 😁 which is the clear color I'm using so far:

• I know this may sound like really not much, but for me this is a significant step anyway: it was pretty complex to get to this point in fact 😅 But I have good hopes things will become easier now (because I already have a lot of support classes, and some structure emerging in my engine).
• Actually, let's continue with trying to check this default clear color… Oh my, that was super easy: I just need to set the clear color of the attachment 0 of each of the framebuffers I'm using (I areally only added those 3 lines in lua!):
        -- Set the clear color of the first attachment of the framebuffer:
local fbuf = self.vkeng:get_framebuffer(fbIdx)
fbuf:set_clear_color(0, 1.0, 0.0, 0.0, 1.0)
• And here we go!

• Let's continue with the rendering of a first triangle. In theory, all I need for that is a draw call in the command buffers.
• (⇒ OOooops, no, I also need to bind the graphics pipeline of course, thank you dear validation layer 😂)
• So the final updates to the command buffers are as follow:
        cbuf:begin(0)

-- Begin rendering into the swapchain framebuffer:
cbuf:begin_inline_pass(rpassIdx, fbIdx)

-- Bind the graphics pipeline:
cbuf:bind_graphics_pipeline(pipelineIdx)

-- Draw our triangle:
cbuf:draw(3)

-- End the render pass:
cbuf:end_render_pass()

-- Finish the command buffer:
cbuf:finish()
• ⇒ And again, it's working without too much trouble!

• For once, this is getting a bit exciting!
• Arrff, this didn't remain exciting very long lol… now just trying to change the color of my triangle to something else than red, and that doesn't seem to work as expected: the triangle remains red, or becomes some kind of dark blue… What did I mess 🥴 ??
• ⇒ First I have the fact that I'm sharing a single depth image between all framebuffers, lets fix that ⇒ now creating the depth image per framebuffer::
    for i = 0, num - 1 do

-- Before we can create a framebuffer, we need to create the depth image that we will use as attachment:
local depthImg = self.vkeng:create_image_2d(self.width, self.height, vk.Format.D32_SFLOAT,
vk.ImageUsageFlagBits.DEPTH_STENCIL_ATTACHMENT_BIT)
-- logDEBUG("Depth image idx: ", depthImg)
self:allocate_image_memory(depthImg)

local depthView = self.vkeng:create_image_view(depthImg)

-- Alos get the view for the swapchain image:
local imgIdx = schain:get_image(i)

-- self:allocate_image_memory(imgIdx)
local viewIdx = self.vkeng:create_image_view(imgIdx)

-- Create the framebuffer that will be used to render into the image (i):
local fbIdx = self.vkeng:create_framebuffer(rpassIdx, { viewIdx, depthView }, self.width, self.height)
self.framebuffers:push_back(fbIdx)
end
• ⇒ But this doesn't fix our color issue of course. And in fact, looking more carefully, the triangle doesn't really look like it's full red… it rather seems to be mixed with the background color a bit ? It becomes full red if I set the background color to (0,0,0,1)
• Also, I was not setting the VkSubpassDependency properly when creating the render pass, fixing that:
    local ddesc = nvk.VulkanSubpassDependency()
vk.PipelineStageFlagBits.EARLY_FRAGMENT_TESTS_BIT)
vk.PipelineStageFlagBits.EARLY_FRAGMENT_TESTS_BIT)
vk.AccessFlagBits.DEPTH_STENCIL_ATTACHMENT_WRITE_BIT)

• But still, my triangle is just red 😭
• ⇒ As I said above, this really looks like a blending issue, so let's check our settings at that level now.
• Hmmm, actually, just noticed that the usrface format is set to B8G8R8A8_SRGB in lve, let's try that one. Nope, not helping.
• But my idea above about blending was correct: if I completely comment the ColorBlendState config from the pipeline config creation, then the display colors are OK 😲! So let's see what I could be doing wrong there.
• Note: actually there is a validation error if we do not specify the ColorBlendState info:
VUID-VkGraphicsPipelineCreateInfo-renderPass-06044(ERROR / SPEC): msgNum: -1526981024 - Validation Error: [ VUID-VkGraphicsPipelineCreateInfo-renderPass-06044 ] Object 0: handle = 0x27a7263ab50, type = VK_OBJECT_TYPE_DEVICE; | MessageID = 0xa4fc1e60 | Invalid Pipeline CreateInfo[0] State: pColorBlendState is NULL when rasterization is enabled and subpass uses color attachments. The Vulkan spec states: If renderPass is not VK_NULL_HANDLE, the pipeline is being created with fragment output interface state, and subpass uses color attachments, pColorBlendState must be a valid pointer to a valid VkPipelineColorBlendStateCreateInfo structure (https://vulkan.lunarg.com/doc/view/1.3.224.1/windows/1.3-extensions/vkspec.html#VUID-VkGraphicsPipelineCreateInfo-renderPass-06044)
• Tried to write on ColorBlendState struct directly from C++ and this works:
auto VulkanDevice::create_graphics_pipelines(
const VkGraphicsPipelineCreateInfoList& infos, VkPipelineCache cache)
-> VulkanPipelineList {
VulkanPipelineList list;
if (infos.empty())
return list;

// override the ColorBlendState config here:
VkPipelineColorBlendAttachmentState attachment{};

VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
attachment.blendEnable = VK_FALSE;
attachment.srcColorBlendFactor = VK_BLEND_FACTOR_ONE;  // Optional
attachment.dstColorBlendFactor = VK_BLEND_FACTOR_ZERO; // Optional
attachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;  // Optional
attachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; // Optional

VkPipelineColorBlendStateCreateInfo blendInfo{};

blendInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
blendInfo.logicOpEnable = VK_FALSE;
blendInfo.logicOp = VK_LOGIC_OP_COPY; // Optional
blendInfo.attachmentCount = 1;
blendInfo.pAttachments = &attachment;
blendInfo.blendConstants[0] = 0.0F; // Optional
blendInfo.blendConstants[1] = 0.0F; // Optional
blendInfo.blendConstants[2] = 0.0F; // Optional
blendInfo.blendConstants[3] = 0.0F; // Optional

const_cast<VkGraphicsPipelineCreateInfoList&>(infos)[0].pColorBlendState =
&blendInfo;

VkPipelineList pipelines;
pipelines.resize(infos.size());
CHECK_VK_MSG(vkCreateGraphicsPipelines(_device, cache,
(uint32_t)infos.size(), infos.data(),
nullptr, pipelines.data()),
"Cannot create graphic pipelines");

for (auto& p : pipelines) {
list.push_back(new VulkanPipeline(this, p));
}

return list;
}
• Ohh crap… it was the color write mask in the blend attachment… because of the default value not being propagated in the lua bindings 😬!
• When specifying the writeMask value, everything seems OK:
    local blend = cfg:getCurrentColorBlendState()
-- blend:setLogicOpEnabled(vk.FALSE)
blend:setLogicOp(vk.LogicOp.COPY)
-- blend:setBlendConstants(0.0, 0.0, 0.0, 0.0)
vk.BlendFactor.ONE, -- srcColor
vk.BlendFactor.ZERO, -- dstColor
vk.BlendFactor.ONE, -- srcAlphaBlendFactor,
vk.BlendFactor.ZERO, -- dstAlphaBlendFactor,
vk.ColorComponentFlagBits.R_BIT + vk.ColorComponentFlagBits.G_BIT + vk.ColorComponentFlagBits.B_BIT +
vk.ColorComponentFlagBits.A_BIT
)
• ⇒ Stupid me… 😁 ⇒ I really need to pay attention to those default values more closely now.
• Now I finally have my yellow triangle:

• Now checking the generated bindings, here is what I get for the addAttachment method:
inline auto
VkBlendFactor dstColorBlendFactor, VkBlendOp colorBlendOp,
VkBlendFactor srcAlphaBlendFactor,
VkBlendFactor dstAlphaBlendFactor, VkBlendOp alphaBlendOp,
VK_COLOR_COMPONENT_G_BIT |
VK_COLOR_COMPONENT_B_BIT |
VK_COLOR_COMPONENT_A_BIT)
-> class_type& {
colorBlendOp, srcAlphaBlendFactor,
}
• Generated bindings:

static auto _bind_addAttachment_sig2(lua_State* L) -> int {
int luatop = lua_gettop(L);
nvk::VulkanPipelineColorBlendStateCreateInfo* self = Luna< nvk::VulkanPipelineColorBlendStateCreateInfo >::get(L,1);
LUNA_ASSERT(self!=nullptr);

auto blendEnable = (uint32_t)lua_tointeger(L,2);
auto srcColorBlendFactor = (VkBlendFactor)lua_tointeger(L,3);
auto dstColorBlendFactor = (VkBlendFactor)lua_tointeger(L,4);
auto colorBlendOp = (VkBlendOp)lua_tointeger(L,5);
auto srcAlphaBlendFactor = (VkBlendFactor)lua_tointeger(L,6);
auto dstAlphaBlendFactor = (VkBlendFactor)lua_tointeger(L,7);
auto alphaBlendOp = (VkBlendOp)lua_tointeger(L,8);
uint32_t colorWriteMask = luatop>=9? (uint32_t)lua_tointeger(L,9) : (uint32_t)VK_COLOR_COMPONENT_R_BIT;

Luna< nvk::VulkanPipelineColorBlendStateCreateInfo >::push(L, &res, false);

return 1;
}


• ⇒ I should really look into this default value of “VK_COLOR_COMPONENT_R_BIT” here… 😨
• The corresponding AST at that location is pretty complex (but all our elements are in there):
2022-11-04 12:24:20.491163 [DEBUG] cursor AST:

2022-11-04 12:24:20.491199 [DEBUG] level 1: 'VkColorComponentFlags' of kind 'TypeRef', type: 'VkColorComponentFlags'

2022-11-04 12:24:20.491236 [DEBUG] level 1: '' of kind 'UnexposedExpr', type: 'VkColorComponentFlags'

2022-11-04 12:24:20.491273 [DEBUG] level 2: '' of kind 'BinaryOperator', type: 'int'

2022-11-04 12:24:20.491308 [DEBUG] level 3: '' of kind 'BinaryOperator', type: 'int'

2022-11-04 12:24:20.491345 [DEBUG] level 4: '' of kind 'BinaryOperator', type: 'int'

2022-11-04 12:24:20.491383 [DEBUG] level 5: 'VK_COLOR_COMPONENT_R_BIT' of kind 'UnexposedExpr', type: 'int'

2022-11-04 12:24:20.491444 [DEBUG] level 6: 'VK_COLOR_COMPONENT_R_BIT' of kind 'DeclRefExpr', type: 'VkColorComponentFlagBits'

2022-11-04 12:24:20.491488 [DEBUG] level 5: 'VK_COLOR_COMPONENT_G_BIT' of kind 'UnexposedExpr', type: 'int'

2022-11-04 12:24:20.491528 [DEBUG] level 6: 'VK_COLOR_COMPONENT_G_BIT' of kind 'DeclRefExpr', type: 'VkColorComponentFlagBits'

2022-11-04 12:24:20.491570 [DEBUG] level 4: 'VK_COLOR_COMPONENT_B_BIT' of kind 'UnexposedExpr', type: 'int'

2022-11-04 12:24:20.491610 [DEBUG] level 5: 'VK_COLOR_COMPONENT_B_BIT' of kind 'DeclRefExpr', type: 'VkColorComponentFlagBits'

2022-11-04 12:24:20.491696 [DEBUG] level 3: 'VK_COLOR_COMPONENT_A_BIT' of kind 'UnexposedExpr', type: 'int'

2022-11-04 12:24:20.491746 [DEBUG] level 4: 'VK_COLOR_COMPONENT_A_BIT' of kind 'DeclRefExpr', type: 'VkColorComponentFlagBits
• ⇒ I think I need to get access to the “UnexposedExpr” cursor at level 2 above and just try to collect all its tokens ? Let see if this coud work.
• Okay, so, this was a little tricky, but I finally managed to update the default value parsing in NervBind, and now the default value I get for the colorWriteMask is the correct one:
static auto _bind_addAttachment_sig2(lua_State* L) -> int {
int luatop = lua_gettop(L);
nvk::VulkanPipelineColorBlendStateCreateInfo* self = Luna< nvk::VulkanPipelineColorBlendStateCreateInfo >::get(L,1);
LUNA_ASSERT(self!=nullptr);

auto blendEnable = (uint32_t)lua_tointeger(L,2);
auto srcColorBlendFactor = (VkBlendFactor)lua_tointeger(L,3);
auto dstColorBlendFactor = (VkBlendFactor)lua_tointeger(L,4);
auto colorBlendOp = (VkBlendOp)lua_tointeger(L,5);
auto srcAlphaBlendFactor = (VkBlendFactor)lua_tointeger(L,6);
auto dstAlphaBlendFactor = (VkBlendFactor)lua_tointeger(L,7);
auto alphaBlendOp = (VkBlendOp)lua_tointeger(L,8);
uint32_t colorWriteMask = luatop>=9? (uint32_t)lua_tointeger(L,9) : (uint32_t)VK_COLOR_COMPONENT_R_BIT|VK_COLOR_COMPONENT_G_BIT|VK_COLOR_COMPONENT_B_BIT|VK_COLOR_COMPONENT_A_BIT;

}