diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d6d5f8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.vs +thirdparty/* +*.exe +*.o +*.pdb +*.ilk +*.spv +*.lib diff --git a/bin/shaders/src/simple.frag b/bin/shaders/src/simple.frag new file mode 100644 index 0000000..ea99184 --- /dev/null +++ b/bin/shaders/src/simple.frag @@ -0,0 +1,7 @@ +#version 450 + +layout (location = 0) out vec4 outcolour; +void main() +{ + outcolour = vec4(1.0, 0.0, 0.0, 1.0); +} \ No newline at end of file diff --git a/bin/shaders/src/simple.vert b/bin/shaders/src/simple.vert new file mode 100644 index 0000000..e6eb37d --- /dev/null +++ b/bin/shaders/src/simple.vert @@ -0,0 +1,11 @@ +#version 450 + +vec2 positions[3] = { + 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); +} \ No newline at end of file diff --git a/buildscripts/basalt.lua b/buildscripts/basalt.lua new file mode 100644 index 0000000..fe3f138 --- /dev/null +++ b/buildscripts/basalt.lua @@ -0,0 +1,79 @@ +-- TODO: +local vulkan_sdk_dir = os.getenv("VULKAN_SDK") +local args = {...} +local output_name = "basalt.lib" +local include_dirs = "-I\"" .. table.concat({ + "include", + "thirdparty/glfw/include", + vulkan_sdk_dir .. "/Include", + vulkan_sdk_dir .. "/Include/vulkan", +}, "\" -I\"") .. '\"' +local src_dirs = { + "src" +} +local compiler_flags = table.concat({ + "-c", + "-g", + "-O0" +}, ' ') +local library_dirs = '-L\"' .. table.concat({ + +}, '\" -L\"') .. '"' +local library_linker_inputs = '-l' .. table.concat({ + +}, ' -l') +local linker_flags = table.concat({ + 'rc' +}, ' ') + +if include_dirs == "-I\"\"" then include_dirs = "" end +if library_dirs == "-L\"\"" then library_dirs = "" end +if library_linker_inputs == "-l" then library_linker_inputs = "" end + +local compiler = require("buildscripts.helpers.compiler"); + +-- NOTE: This is not scalable +-- It does not care what source folder the object comes from +local function srcobj_mapping(src_file, obj_file) + -- Return the object file that the source file should be associated with + if (src_file ~= nil and obj_file == nil) then + return "obj/" .. src_file .. ".o" + -- Return the source files associated with an object file + elseif (src_file == nil and obj_file ~= nil) then + return {obj_file:sub(5, -3)} + else error("Invalid usage of srcobj mapping function: type(src_file) = " .. type(src_file) .. ", type(obj_file) = " .. type(obj_file)) end +end + +local cpp_extensions = { + ['.cpp']=true, + ['.hpp']=true +} + +local source_files = compiler.get_newer_src_files(src_dirs, cpp_extensions, srcobj_mapping) +local translation_units = compiler.generate_translation_units(source_files, srcobj_mapping) +for i,k in ipairs(translation_units) do + print("Running command clang++.exe " .. compiler_flags .. ' ' .. include_dirs .. ' ' .. table.concat(k.src, " ") .. ' -o ' .. k.obj) +end + + +local err, stdout, stderr = compiler.compile_translation_units("clang++.exe", 4, translation_units, compiler_flags, include_dirs) +for i,ec in ipairs(err) do + if (ec ~= 0) then + print("Invocation ".. tostring(i) .. " failed with exit code " .. tostring(ec) .. " and output;") + print() + print(stderr[i]) + print() + print(stdout[i]) + print() + end +end + +local err, stdout, stderr = compiler.link_translation_units("llvm-ar", {'obj'}, {['.o']=true}, "bin/" .. output_name, library_dirs, library_linker_inputs, linker_flags) +if err ~= 0 then + print("Error linking translation units together") + print() + print(stdout) + print() + print(stderr) + print() +end diff --git a/buildscripts/clean.lua b/buildscripts/clean.lua new file mode 100644 index 0000000..c8d24fb --- /dev/null +++ b/buildscripts/clean.lua @@ -0,0 +1,3 @@ +return function() + -- TODO +end \ No newline at end of file diff --git a/buildscripts/configure.lua b/buildscripts/configure.lua new file mode 100644 index 0000000..0fc0bbe --- /dev/null +++ b/buildscripts/configure.lua @@ -0,0 +1,35 @@ +local function download_requirements() + -- Git module is not yet implemented, so delegate to the command-line + local argv = { + {"clone https://github.com/glfw/glfw.git thirdparty/glfw"} + } + local ec, stdout, stderr = platform.exec_parallel("git", 1, argv) + for i,ec in pairs(ec) do + if (ec ~= 0) then + print("Command ".. tostring(i) .." git " .. table.concat(argv[i], " ") .. "\" failed with exit code " .. tostring(ec) .. " and output;") + print(stderr[i]) + print() + print(stdout[i]) + print() + end + end +end + +local function build_glfw3(...) + fs.create_dir("thirdparty/glfw/bin") + local err, stdout, stderr = platform.exec("cmake", { + "-S", "thirdparty/glfw", + "-B", "thirdparty/glfw/bin", + "-D BUILD_SHARED_LIBS=ON", + "-D GLFW_LIBRARY_TYPE=STATIC", + "-D GLFW_BUILD_TESTS=OFF", + "-D GLFW_BUILD_DOCS=OFF", + --"-D USE_MSVC_RUNTIME_LIBRARY_DLL=ON", + ... + }) + platform.exec("cmake", {"--build thirdparty/glfw/bin"}) + if err ~= 0 then print(stdout); print(); print(stderr); print(); end +end + +download_requirements() +build_glfw3(...) diff --git a/buildscripts/help.lua b/buildscripts/help.lua new file mode 100644 index 0000000..678f8a1 --- /dev/null +++ b/buildscripts/help.lua @@ -0,0 +1,17 @@ +print([[ +Usage: lbs [args...] +Valid subcommands: + lbs configure [args...]: + Performs initial setup of the project, downloading and compiling dependencies (glfw) + lbs basalt [args...]: + Compiles basalt static libarary and outputs it to bin/basalt.lib + lbs test [args...] + Compiles the specified test project and outputs its executable to bin/ + lbs clean [args...] + Deletes any library/binary/object/temp files + If no arguments are provided, deletes *all* files in project meeting requirements above + Otherwise, will clean specified project folders e.g; + basalt Cleans the basalt library + tests/basic Cleans the basic test + thirdparty/glfw Cleans the dependency GLFW +]]) \ No newline at end of file diff --git a/buildscripts/helpers/compiler.lua b/buildscripts/helpers/compiler.lua new file mode 100644 index 0000000..5455ae0 --- /dev/null +++ b/buildscripts/helpers/compiler.lua @@ -0,0 +1,114 @@ +local lib = {} +-- TODO: Specify what should happen if srcobj_mapping_fn returns nil for an object or source file + +function lib.get_object_files(obj_dirs, obj_postfixes) + local objs = {} + if type(obj_dirs) == "string" then obj_dirs = {obj_dirs} end + for _,dir in ipairs(obj_dirs) do + for fsobj in fs.forall(dir) do + if not fs.is_dir(fsobj) and obj_postfixes[fs.extension(fsobj)] then + objs[#objs+1] = fsobj + end + end + end + return objs +end + +--- Generates a list of source files that are newer than their corresponding object and returns it +---@param dirs string[] An array of source directories to scan +---@param src_postfixes {[string]: boolean} A map of source file extensions to a boolean value +---@param srcobj_mapping_fn fun(src_file: string | nil, obj_file: string | nil): string Called with (src_file, nil), should return object file path or nil. nil assumes source is newer, otherwise it is checked +---@return string[] An array of source files that are newer than their corresponding object file, according to srcobj_mapping_fn +function lib.get_newer_src_files(dirs, src_postfixes, srcobj_mapping_fn) + local srcs = {} + if type(dirs) == "string" then dirs = {dirs} end + for _,dir in ipairs(dirs) do + for fsobj in fs.forall(dir) do + local src_file = fsobj + if not fs.is_dir(fsobj) then repeat + if not src_postfixes[fs.extension(src_file)] then break end + -- Only bother checking if the source is newer than its corresponding obj file if it has a mapping, + -- Otherwise queue it to be potentially recompiled + local obj = srcobj_mapping_fn(src_file, nil) + if obj ~= nil then + if not fs.is_newer(src_file, obj) then break end + end + srcs[#srcs+1] = src_file + until true end + end + end + return srcs +end + +--- Given a list of source files and a bi-directional src-obj mapping function, will return a list of translation units +---@param src_list string[] A list of source files to be included +---@param srcobj_mapping_fn fun(src_file: string | nil, obj_file: string | nil): string A bi-directional mapping function that will return the object a source is related to or the sources that are related to an object +---@return {obj: string, src: string[]}[] Returns a list of translation units, describing the source files that go into generating a particular object file +function lib.generate_translation_units(src_list, srcobj_mapping_fn) + local srcs_handled = {} + local units = {} + for _, src_file in ipairs(src_list) do repeat + if (srcs_handled[src_file]) then break end + local obj = srcobj_mapping_fn(src_file, nil) + units[#units+1] = {} + units[#units].obj = obj + units[#units].src = srcobj_mapping_fn(nil, obj) + for _,src in ipairs(units[#units].src) do + srcs_handled[src] = true + end + until true end + return units +end + + +--- Compiles a list of translation units using cmd, appending any additional arguments to all commands +--- This compilation occurs in parallel with up to num_parallel simultanious processes +---@param cmd string The command to use in order to compile a translation unit +---@param num_parallel integer Maximum number of processes that may exist at one time +---@param translation_units {obj: string, src: string[]}[] An array of translation units, each containing a list of one or more source files and an output file +---@param ... string additional arguments to add to all invocation of the compiler +---@return integer[] return codes from all processes dispatched +---@return string[] stdout output from all processes dispatched +---@return string[] stderr output from all processes dispatched +function lib.compile_translation_units(cmd, num_parallel, translation_units, ...) + local argv = {} + local mirror_args = table.concat({...}, " ") + for _,unit in ipairs(translation_units) do + fs.create_dir(fs.dir(unit.obj)) + argv[#argv+1] = {table.concat({mirror_args, unpack(unit.src), "-o", unit.obj}, ' ')} + --print("clang++.exe " .. table.concat(argv[#argv], ' ')) + end + return platform.exec_parallel(cmd, num_parallel, argv) +end + +--- Runs a single invocation of cmd with the obj component of every translation unit appended +---@param cmd string The command to be executed +---@param obj_postfixes {[string]: boolean | nil} a list of postfixes to look for in the object directories +---@param output string the filename and path for the output file +---@param ... string additional arguments provided to the command +function lib.link_translation_units(cmd, obj_dirs, obj_postfixes, output, ...) + local inputs = {...} + local obj_files = lib.get_object_files(obj_dirs, obj_postfixes) + inputs[#inputs+1] = output + inputs[#inputs+1] = table.concat(obj_files, ' ') + print("Executing " .. cmd .. ' ' .. table.concat(inputs, ' ')) + return platform.exec(cmd, inputs) +end + +function lib.print_errors(err, stdout, stderr) + if type(err) == "number" then err = {err} end + if type(stdout) == "string" then stdout = {stdout} end + if type(stderr) == "string" then stderr = {stderr} end + for i,ec in ipairs(err) do + if (ec ~= 0) then + print("Invocation ".. tostring(i) .. " failed with exit code " .. tostring(ec) .. " and output;") + print() + print(stderr[i]) + print() + print(stdout[i]) + print() + end + end +end + +return lib diff --git a/buildscripts/tests.lua b/buildscripts/tests.lua new file mode 100644 index 0000000..593a209 --- /dev/null +++ b/buildscripts/tests.lua @@ -0,0 +1,98 @@ +local vulkan_sdk_dir = os.getenv("VULKAN_SDK") + +local args = {...} +args[1] = args[1] or "basic" +if type(args[1]) ~= "string" or not fs.is_dir("tests/"..args[1]) then args[1] = "basic" end +local output_name = args[1] .. '.exe' + +local include_dirs = "-I\"" .. table.concat({ + "include", + "thirdparty/glfw/include", + vulkan_sdk_dir .. "/Include", + vulkan_sdk_dir .. "/Include/vulkan", + "tests/" .. args[1] .. '/include' +}, "\" -I\"") .. '"' +local src_dirs = { + "tests/" .. args[1] .. "/src" +} +local obj_dirs = { + "tests/" .. args[1] .. "/obj" +} +local compiler_flags = table.concat({ + "-c", + "-g", + "-O0" +}, ' ') +local library_dirs = '-L\"' .. table.concat({ + "bin/", + "thirdparty/glfw/bin/src", + vulkan_sdk_dir .. "/Lib" +}, '\" -L\"') .. '"' +local library_linker_inputs = '-l' .. table.concat({ + "basalt", + "glfw3", + "vulkan-1", + -- Windows + "msvcrtd", + "user32", + "gdi32", + "shell32" + -- Linux +}, ' -l') +local linker_flags = table.concat({ + +}, ' ') +local shader_src_dir = "bin/shaders/src" +local shader_out_dir = "bin/shaders/out" + +if include_dirs == "-I\"\"" then include_dirs = "" end +if library_dirs == "-L\"\"" then library_dirs = "" end +if library_linker_inputs == "-l" then library_linker_inputs = "" end + +local cpp_extensions = { + ['.cpp']=true, + ['.hpp']=true +} + +local compiler = require("buildscripts.helpers.compiler"); + +-- NOTE: This is not scalable +-- It does not care what source folder the object comes from +local function srcobj_mapping(src_file, obj_file) + local base = "./tests/".. args[1] + -- Return the object file that the source file should be associated with + if (src_file ~= nil and obj_file == nil) then + -- Make the source file relative to the test root directory' + local file = src_file .. '.o' + return base .. '/obj/' .. fs.relative_to(file, base) + -- Return the source files associated with an object file + elseif (src_file == nil and obj_file ~= nil) then + return {base .. '/' .. obj_file:sub(#base+6, -3)} + else error("Invalid usage of srcobj mapping function: type(src_file) = " .. type(src_file) .. ", type(obj_file) = " .. type(obj_file)) end +end + + +-- Get translation units +local source_files = compiler.get_newer_src_files(src_dirs, cpp_extensions, srcobj_mapping) +local translation_units = compiler.generate_translation_units(source_files, srcobj_mapping) + +-- Compile the binary +compiler.print_errors(compiler.compile_translation_units("clang++.exe", 4, translation_units, compiler_flags, include_dirs)) + +-- Link binary +compiler.print_errors(compiler.link_translation_units("clang++.exe", obj_dirs, {['.o']=true}, "-o bin/" .. output_name, library_dirs, library_linker_inputs, linker_flags)) + +-- Build shaders +local function shader_srcobj_mapping(src_file, obj_file) + if (src_file ~= nil and obj_file == nil) then + return fs.dir(src_file):sub(1,-5) .. 'out/' .. fs.file_name(src_file) .. '.spv' + elseif (src_file == nil and obj_file ~= nil) then + return { fs.dir(obj_file):sub(1,-5) .. 'src/' .. fs.file_name(obj_file):sub(1,-#".spv"-1) } + + else error("Invalid usage of srcobj mapping function for shader compilation") end +end + +-- Get source files +local shader_sources = compiler.get_newer_src_files(shader_src_dir, {[".frag"]=true,[".vert"]=true,[".glsl"]=true}, shader_srcobj_mapping) +local shader_translation_units = compiler.generate_translation_units(shader_sources, shader_srcobj_mapping) +compiler.print_errors(compiler.compile_translation_units("glslc", 4, shader_translation_units)) diff --git a/include/basalt_context.h b/include/basalt_context.h new file mode 100644 index 0000000..c79f49f --- /dev/null +++ b/include/basalt_context.h @@ -0,0 +1,10 @@ +#pragma once +#include + +namespace basalt +{ + class Context + { + + }; +} \ No newline at end of file diff --git a/include/basalt_window.h b/include/basalt_window.h new file mode 100644 index 0000000..9eb07eb --- /dev/null +++ b/include/basalt_window.h @@ -0,0 +1,29 @@ +#pragma once +#define GLFW_INCLUDE_VULKAN +#include "GLFW/glfw3.h" + +namespace basalt +{ + class Window + { + public: + Window(const Window& src) = delete; + Window& operator =(const Window& src) = delete; + Window(Window&& other) noexcept; + Window& operator =(Window&& other) noexcept; + + Window(uint16_t width, uint16_t height, const char* title); + ~Window(void) noexcept; + + void swap(Window& other); + operator GLFWwindow* (void) const noexcept; + + const bool should_close(void) const noexcept; + + protected: + GLFWwindow* window = nullptr; + const char* window_title = "N/A"; + uint16_t width = 0; + uint16_t height = 0; + }; +} \ No newline at end of file diff --git a/lbs.lua b/lbs.lua new file mode 100644 index 0000000..7a588b9 --- /dev/null +++ b/lbs.lua @@ -0,0 +1,21 @@ +basalt = function() require("buildscripts.basalt") end +test = function() require("buildscripts.tests") end +configure = function() require("buildscripts.configure") end +help = function() require("buildscripts.help") end +clean = function() require("buildscripts.clean") end + +function main(cmd, ...) + local args = {} + --args[#args+1] = {} + args[#args+1] = {"--version"} + args[#args+1] = {"lbs.lua"} + local ec, stdout, stderr = platform.exec_parallel("clang", 2, args) + for i,e in ipairs(ec) do + print(e) + print("Standard output;") + print(stdout[i]) + print("Standard error;") + print(stderr[i]) + end +end +return true \ No newline at end of file diff --git a/src/basalt_context.cpp b/src/basalt_context.cpp new file mode 100644 index 0000000..28d4bb2 --- /dev/null +++ b/src/basalt_context.cpp @@ -0,0 +1,7 @@ +#include "basalt_context.h" +#include "stdio.h" + +void test() +{ + printf("Hello, world!\n"); +} \ No newline at end of file diff --git a/src/basalt_window.cpp b/src/basalt_window.cpp new file mode 100644 index 0000000..d5a17ea --- /dev/null +++ b/src/basalt_window.cpp @@ -0,0 +1,70 @@ +#include "basalt_window.h" + +basalt::Window::Window(Window&& other) noexcept +{ + this->window = other.window; + this->window_title = other.window_title; + this->height = other.height; + this->width = other.width; + other.height = 0; + other.width = 0; + other.window = nullptr; + other.window_title = nullptr; +} + +basalt::Window& basalt::Window::operator=(Window&& other) noexcept +{ + this->~Window(); + this->window = other.window; + this->window_title = other.window_title; + this->height = other.height; + this->width = other.width; + other.height = 0; + other.width = 0; + other.window = nullptr; + other.window_title = nullptr; + return *this; +} + +basalt::Window::Window(uint16_t width, uint16_t height, const char* title) : + width(width), height(height), window_title(title) +{ + glfwInit(); + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); + glfwWindowHint(GLFW_TRANSPARENT_FRAMEBUFFER, GLFW_TRUE); + this->window = glfwCreateWindow(width, height, title, nullptr, nullptr); +} + +basalt::Window::~Window(void) noexcept +{ + glfwDestroyWindow(this->window); +} + +void basalt::Window::swap(Window& other) +{ + { + uint16_t tmp = this->height; + this->height = other.height; + other.height = tmp; + tmp = this->width; + this->width = other.width; + other.width = tmp; + } + { + void* tmp_ptr = this->window; + this->window = other.window; + other.window = static_cast(tmp_ptr); + tmp_ptr = const_cast(this->window_title); + this->window_title = other.window_title; + other.window_title = static_cast(tmp_ptr); + } +} + +basalt::Window::operator GLFWwindow* (void) const noexcept +{ return this->window; } + +const bool basalt::Window::should_close(void) const noexcept +{ + return glfwWindowShouldClose(this->window); +} diff --git a/tests/basic/src/main.cpp b/tests/basic/src/main.cpp new file mode 100644 index 0000000..f801320 --- /dev/null +++ b/tests/basic/src/main.cpp @@ -0,0 +1,13 @@ +#include "basalt_context.h" +#include "basalt_window.h" + +int main() +{ + basalt::Window window(640, 480, "Hello Vulkan!"); + while (!window.should_close()) + { + glfwPollEvents(); + + } + return 0; +}