diff --git a/CMakeLists.txt b/CMakeLists.txt index dacab089..4848e612 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -215,6 +215,10 @@ endif () # Compile Tools # ----------------------------------------------------------------------------- message(STATUS "[Ballistic] Configuring developer tools...") +set(CMAKE_BUILD_RPATH_USE_ORIGIN TRUE) +list(APPEND CMAKE_BUILD_RPATH "$ORIGIN/../../lib/${BALLISTIC_PLATFORM}") +add_external_subdirectory(extern/LuaJIT-2.1) + set(DECODER_CLI_NAME "decoder_cli") add_executable(${DECODER_CLI_NAME} tools/decoder_cli.c) target_link_libraries(${DECODER_CLI_NAME} PRIVATE ${PROJECT_NAME}) @@ -333,7 +337,6 @@ if (BALLISTIC_BUILD_TESTS) gtest_discover_tests(${target_name}) endforeach () - add_external_subdirectory(extern/LuaJIT-2.1) add_test( NAME doctest diff --git a/extern/glfw b/extern/glfw index b00e6a8a..567b1ec2 160000 --- a/extern/glfw +++ b/extern/glfw @@ -1 +1 @@ -Subproject commit b00e6a8a88ad1b60c0a045e696301deb92c9a13e +Subproject commit 567b1ec2442d59525e24c19e8d413df6baf02496 diff --git a/tools/dashboard/CMakeLists.txt b/tools/dashboard/CMakeLists.txt index abc30fb2..f1e38645 100644 --- a/tools/dashboard/CMakeLists.txt +++ b/tools/dashboard/CMakeLists.txt @@ -7,6 +7,20 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) +set(FFI_GENERATOR_SCRIPT ${CMAKE_SOURCE_DIR}/tools/generate_ffi.lua) +set(DASHBOARD_HEADER ${CMAKE_CURRENT_SOURCE_DIR}/dashboard_file_dialog.h) +set(GENERATED_FFI_BINDINGS ${CMAKE_CURRENT_SOURCE_DIR}/scripts/generated/dashboard_file_dialog_ffi.lua) + +add_custom_command( + OUTPUT ${GENERATED_FFI_BINDINGS} + COMMAND $ "${FFI_GENERATOR_SCRIPT}" "${DASHBOARD_HEADER}" "${GENERATED_FFI_BINDINGS}" + DEPENDS ${FFI_GENEERATOR_SCRIPT} ${DASHBOARD_HEADER} luajit_exe + COMMENT " [Dashboard] Generating FFI bindings from hashboard_file_dialog.h" + WORKING_DIRECTORY ${BALLISTIC_LIBRARIES_DIRECTORY} + VERBATIM +) + +add_custom_target(dashboard_ffi_bindings ALL DEPENDS ${GENERATED_FFI_BINDINGS}) add_executable(${PROJECT_NAME} main.c dashboard_backend.cpp @@ -32,9 +46,8 @@ target_include_directories(${PROJECT_NAME} PRIVATE ${glfw_SOURCE_DIR}/include ) -# TODO: find_package(OpenGL) for macOS. +add_dependencies(${PROJECT_NAME} dashboard_ffi_bindings) target_link_libraries(${PROJECT_NAME} PRIVATE Ballistic glfw cimgui_glfw LuaJIT) - set_target_properties(${PROJECT_NAME} PROPERTIES ENABLE_EXPORTS TRUE) if (CMAKE_SYSTEM_NAME STREQUAL "Windows") diff --git a/tools/dashboard/scripts/dashboard.lua b/tools/dashboard/scripts/dashboard.lua index 7f95d1d1..2d960b4f 100644 --- a/tools/dashboard/scripts/dashboard.lua +++ b/tools/dashboard/scripts/dashboard.lua @@ -6,35 +6,111 @@ package.path = package.path .. ";" .. script_dir .. "?.lua" local ffi = require("ffi") local imgui = require("imgui.glfw") +local dialog_ffi = require("generated.dashboard_file_dialog_ffi") +local dashboard = dialog_ffi.Dashboard -ffi.cdef [[ -typedef struct -{ - char name[248]; - uint32_t is_directory; - uint32_t pad; -} bal_file_entry_t; +local function dashboard_render_file_dialog(p_file_dialog) + if p_file_dialog == nil then + return false + end -typedef struct -{ - bal_file_entry_t *file_entries; - uint32_t file_entries_count; - uint32_t file_entries_capacity; - bool is_open; - bool just_opened; - char current_path[1024]; - char selected_path[1024]; - uint8_t pad[46]; -} bal_file_dialog_t; + local dialog = ffi.cast("bal_file_dialog_t*", p_file_dialog) -void dashboard_file_dialog_open(bal_file_dialog_t *dialog); -void dashboard_file_dialog_refresh(bal_file_dialog_t *dialog); -void dashboard_file_dialog_append_path(bal_file_dialog_t *dialog, const char *name); -void dashboard_file_dialog_navigate_home(bal_file_dialog_t *dialog); -const char *dashboard_file_dialog_get_current_path(const bal_file_dialog_t *dialog); -]] + if dialog.just_opened then + imgui.OpenPopup("Select Directory") + dialog.just_opened = false + end -local C = ffi.C + local result = false + local is_open = ffi.new("bool[1]", dialog.is_open) + imgui.SetNextWindowSize(imgui.ImVec2(600, 400), imgui.lib.ImGuiCond_Appearing) + + if imgui.BeginPopupModal("Select Directory", is_open, imgui.lib.ImGuiWindowFlags_NoCollapse) then + dialog.is_open = is_open[0] + imgui.TextUnformatted("Path:") + imgui.SameLine() + local back_button_width = imgui.CalcTextSize("Back").x + (imgui.GetStyle().FramePadding.x * 2.0) + local ballistic_button_width = imgui.CalcTextSize("*").x + (imgui.GetStyle().FramePadding.x * 2.0) + local spacing = imgui.GetStyle().ItemSpacing.x + local text_width = imgui.GetContentRegionAvail().x - back_button_width - ballistic_button_width - (spacing * 2) + imgui.SetNextItemWidth(text_width) + imgui.PushStyleColor(imgui.lib.ImGuiCol_ChildBg, imgui.GetStyleColorVec4(imgui.lib.ImGuiCol_FrameBg)[1]) + imgui.PushStyleVar(imgui.lib.ImGuiStyleVar_ChildRounding, imgui.GetStyle().FrameRounding) + local input_flags = bit.bor(imgui.lib.ImGuiInputTextFlags_EnterReturnsTrue, imgui.lib + .ImGuiInputTextFlags_AutoSelectAll) + + if imgui.InputText("##path_input", dialog.current_path, 1024, input_flags) then + imgui.SetKeyboardFocusHere(-1) + dashboard.dashboard_file_dialog_refresh(dialog) + end + + imgui.SameLine() + + if imgui.Button("*") then + dashboard.dashboard_file_dialog_navigate_home(dialog) + end + + imgui.SameLine() + + if imgui.Button("Back") then + dashboard.dashboard_file_dialog_append_path(dialog, "..") + dashboard.dashboard_file_dialog_refresh(dialog) + end + + imgui.PopStyleColor() + imgui.PopStyleVar() + imgui.Separator() + local child_height = -imgui.GetFrameHeightWithSpacing() - 10 + imgui.BeginChild("##dir_list", imgui.ImVec2(0, child_height), true) + + for i = 0, dialog.file_entries_count - 1 do + local entry = dialog.file_entries[i] + imgui.PushStyleVar(imgui.lib.ImGuiStyleVar_SelectableTextAlign, imgui.ImVec2(0, 0.5)) + imgui.PushID(i) + local name = ffi.string(entry.name) + + if name == ".." then + if imgui.Selectable("[..]", false, 0, imgui.ImVec2(0, 20)) then + dashboard.dashboard_file_dialog_append_path(dialog, "..") + dashboard.dashboard_file_dialog_refresh(dialog) + end + else + if imgui.Selectable(name, false, imgui.lib.ImGuiSelectableFlags_AllowDoubleClick, imgui.ImVec2(0, 20)) + then + if imgui.IsMouseDoubleClicked(imgui.lib.ImGuiMouseButton_Left) then + dashboard.dashboard_file_dialog_append_path(dialog, name) + dashboard.dashboard_file_dialog_refresh(dialog) + end + end + end + + imgui.PopID() + imgui.PopStyleVar() + end + + imgui.EndChild() + + if imgui.Button("Select Current Directory") then + ffi.copy(dialog.selected_path, dialog.current_path, 1024) + result = true + dialog.is_open = false + imgui.CloseCurrentPopup() + end + + imgui.SameLine() + + if imgui.Button("Cancel") then + dialog.is_open = false + imgui.CloseCurrentPopup() + end + + imgui.EndPopup() + else + dialog.is_open = is_open[0] + end + + return result +end local function dashboard_render_file_dialog(p_file_dialog) if p_file_dialog == nil then @@ -167,14 +243,14 @@ function dashboard_render(host_context, p_file_dialog) imgui.PushStyleVar(imgui.lib.ImGuiStyleVar_FrameRounding, 8.0) if imgui.Button("Generate Minimal Working Example", imgui.ImVec2(button_width, button_height)) then - C.dashboard_file_dialog_open(p_file_dialog) + dashboard.dashboard_file_dialog_open(p_file_dialog) end imgui.PopStyleColor(3) imgui.PopStyleVar(1) if dashboard_render_file_dialog(p_file_dialog) then - local selected_path = ffi.string(C.dashboard_file_dialog_get_current_path(p_file_dialog)) + local selected_path = ffi.string(dashboard.dashboard_file_dialog_get_current_path(p_file_dialog)) print("Directory chosen: " .. selected_path) end diff --git a/tools/dashboard/scripts/generated/dashboard_file_dialog_ffi.lua b/tools/dashboard/scripts/generated/dashboard_file_dialog_ffi.lua new file mode 100644 index 00000000..75980225 --- /dev/null +++ b/tools/dashboard/scripts/generated/dashboard_file_dialog_ffi.lua @@ -0,0 +1,62 @@ +-- Auto-generated FFI bindings. DO NOT EDIT. +-- Source: /home/apollo/dev/ballistic/tools/dashboard/dashboard_file_dialog.h +-- Generated by tools/generate_ffi.lua + +local ffi = require('ffi') + +ffi.cdef [[ +typedef struct { char name[248]; uint32_t is_directory; uint32_t pad; } bal_file_entry_t; + +typedef struct { bal_file_entry_t *file_entries; uint32_t file_entries_count; uint32_t file_entries_capacity; bool is_open; bool just_opened; char current_path[1024]; char selected_path[1024]; uint8_t pad[46]; } bal_file_dialog_t; + +void dashboard_file_dialog_init(bal_file_dialog_t *dialog); +void dashboard_file_dialog_shutdown(bal_file_dialog_t *dialog); +void dashboard_file_dialog_open(bal_file_dialog_t *dialog); +void dashboard_file_dialog_refresh(bal_file_dialog_t *dialog); +void dashboard_file_dialog_append_path(bal_file_dialog_t *dialog, const char *name); +void dashboard_file_dialog_navigate_home(bal_file_dialog_t *dialog); +const char *dashboard_file_dialog_get_current_path(const bal_file_dialog_t *dialog); +]] + +local M = {} +local C = ffi.C + +local bal_file_entry_t_mt = { __index = {} } +bal_file_entry_t_mt.__index.__type = 'bal_file_entry_t' +ffi.metatype('bal_file_entry_t', bal_file_entry_t_mt) + +local bal_file_dialog_t_mt = { __index = {} } +bal_file_dialog_t_mt.__index.__type = 'bal_file_dialog_t' +ffi.metatype('bal_file_dialog_t', bal_file_dialog_t_mt) + +M.Dashboard = M.Dashboard or {} +function M.Dashboard.dashboard_file_dialog_init(dialog) + return C.dashboard_file_dialog_init(dialog) +end + +M.Dashboard = M.Dashboard or {} +function M.Dashboard.dashboard_file_dialog_shutdown(dialog) + return C.dashboard_file_dialog_shutdown(dialog) +end + +M.Dashboard = M.Dashboard or {} +function M.Dashboard.dashboard_file_dialog_open(dialog) + return C.dashboard_file_dialog_open(dialog) +end + +M.Dashboard = M.Dashboard or {} +function M.Dashboard.dashboard_file_dialog_refresh(dialog) + return C.dashboard_file_dialog_refresh(dialog) +end + +M.Dashboard = M.Dashboard or {} +function M.Dashboard.dashboard_file_dialog_append_path(dialog, name) + return C.dashboard_file_dialog_append_path(dialog, name) +end + +M.Dashboard = M.Dashboard or {} +function M.Dashboard.dashboard_file_dialog_navigate_home(dialog) + return C.dashboard_file_dialog_navigate_home(dialog) +end + +return M diff --git a/tools/generate_ffi.lua b/tools/generate_ffi.lua new file mode 100644 index 00000000..77847ab1 --- /dev/null +++ b/tools/generate_ffi.lua @@ -0,0 +1,165 @@ +--[[ + C-to-LuaJIT FFI Bindings Generator + + Usage: luajit generate_ffi.lua +]] + + + +local ffi = require("ffi") + +local args = { ... } + +if #args < 2 then + io.stderr:write("Usage: luajit generate_ffi.lua \n") + os.exit(1) +end + +local header_path = args[1] +local output_path = args[2] +local file = assert(io.open(header_path, "r"), "Failed to open header: " .. header_path) +local header_content = file:read("*a") +file:close() + +local defines = {} + +for name, value in header_content:gmatch("#define%s+([%w_]+)%s+([%w_]+)") do + if tonumber(value) or defines[value] then + defines[name] = value + end +end + +-- Resolve nested defines. +local changed = true + +while changed do + changed = false + + for k, v in pairs(defines) do + if not tonumber(v) and defines[v] then + defines[k] = defines[v] + changed = true + end + end +end + +local function clean_for_ffi(str) + for name, value in pairs(defines) do + str = str:gsub("%f[%w_]" .. name .. "%f[^%w_]", value) + end + + str = str:gsub("BAL_ALIGNED%s*%b()", "") + str = str:gsub("BAL_EXPORT", "") + str = str:gsub("BAL_RESTRICT", "") + str = str:gsub("BAL_HOT", "") + str = str:gsub("BAL_COLD", "") + str = str:gsub("static_assert%s*%b();?", "") + str = str:gsub("_Static_assert%s*%b();?", "") + str = str:gsub("%s+", " ") + str = str:gsub("^%s+", ""):gsub("%s+$", "") + return str +end + +local exported_functions = {} + +for function_declaration in header_content:gmatch("BAL_EXPORT%s+([^;]+;)") do + table.insert(exported_functions, clean_for_ffi(function_declaration)) +end + +local clean_header = header_content +clean_header = clean_header:gsub("BAL_ALIGNED%s*%b()", "") +clean_header = clean_header:gsub("static_assert%s*%b();?", "") +clean_header = clean_header:gsub("_Static_assert%s*%b();?", "") + +local struct_definitions = {} + +for struct_block in clean_header:gmatch("(typedef%s+struct[%s%w_]*%b{}[%s%w_]*;)") do + table.insert(struct_definitions, clean_for_ffi(struct_block)) +end + +local validation_buffer = {} + +for _, struct in ipairs(struct_definitions) do + table.insert(validation_buffer, struct) +end + +for _, fn in ipairs(exported_functions) do + table.insert(validation_buffer, fn) +end + +local validation_string = table.concat(validation_buffer, "\n") + +if validation_string == "" then + io.stderr:write("Warning: No structs or functions extracted from " .. header_path .. "\n") +end + +local ok, err = pcall(ffi.cdef, validation_string) +if not ok then + io.stderr:write("FFI validation failed for " .. header_path .. ": " .. tostring(err) .. "\n") + io.stderr:write("--- Generated C Definitions ---\n") + io.stderr:write(tostring(validation_string) .. "\n") + io.stderr:write("-------------------------------\n") + os.exit(1) +end + +local out = assert(io.open(output_path, "w"), "Failed to open output: " .. output_path) +out:write("-- Auto-generated FFI bindings. DO NOT EDIT.\n") +out:write("-- Source: " .. header_path .. "\n") +out:write("-- Generated by tools/generate_ffi.lua\n\n") +out:write("local ffi = require('ffi')\n\n") +out:write("ffi.cdef [[\n") + +for _, struct in ipairs(struct_definitions) do + out:write(struct .. "\n\n") +end + +for _, fn in ipairs(exported_functions) do + out:write(fn .. "\n") +end + +out:write("]]\n\n") +out:write("local M = {}\n") +out:write("local C = ffi.C\n\n") + +for _, struct in ipairs(struct_definitions) do + local type_name = struct:match("}%s*([%w_]+)%s*;") + + if type_name then + out:write(string.format("local %s_mt = { __index = {} }\n", type_name)) + out:write(string.format("%s_mt.__index.__type = '%s'\n", type_name, type_name)) + out:write(string.format("ffi.metatype('%s', %s_mt)\n\n", type_name, type_name)) + end +end + +for _, fn in ipairs(exported_functions) do + local return_type, function_name, parameters = fn:match("^(%S+)%s+([%w_]+)%s*%((.*)%)%s*;") + + if function_name then + local namespace = function_name:match("^([%w_]-)_[%w_]+$") or "Global" + local pascal_namespace = namespace:gsub("_(%w)", string.upper):gsub("^(%w)", string.upper) + out:write(string.format("M.%s = M.%s or {}\n", pascal_namespace, pascal_namespace)) + out:write(string.format("function M.%s.%s(", pascal_namespace, function_name)) + + local parameter_names = {} + + if parameters and parameters ~= "void" and parameters ~= "" then + for parameter in parameters:gmatch("[^,]+") do + local name = parameter:match("([%w_]+)%s*$") or parameter:match("%*([%w_]+)") + + if name then + table.insert(parameter_names, name) + end + end + end + + out:write(table.concat(parameter_names, ", ") .. ")\n") + out:write(string.format(" return C.%s(%s)\n", function_name, table.concat(parameter_names, ", "))) + out:write("end\n\n") + end +end + +out:write("return M\n") +out:close() + +io.stdout:write(string.format("[generate_ffi] Generated %d structs, %d functions -> %s\n", #struct_definitions, + #exported_functions, output_path)) \ No newline at end of file