From e538cf55c277b73d9b8cae00ec6dd312e691e0ea Mon Sep 17 00:00:00 2001 From: Raydelto Hernandez Date: Wed, 7 Oct 2026 16:00:42 -0300 Subject: [PATCH 1/9] NAB-6: Linux build system, DXC sub-build and nsc install (1/2) (#1) * NAB-6: add cached submodule hydration helper for Linux worktrees Clones every submodule (recursively) from an already hydrated local checkout instead of GitHub, carries over cache-local attribute overrides and verifies the work trees are actually populated. Co-Authored-By: Claude Opus 5.5 Co-authored-by: multica-agent * NAB-6: forward toolchain to the DXC sub-build, work around DXC fork Linux breakage The DXC configure is a separate CMake invocation, so forward compiler, linker type, ccache launcher and LLVM_PARALLEL_LINK_JOBS (new NBL_DXC_PARALLEL_LINK_JOBS, default 1) explicitly. On non-Windows hosts also work around two problems in the Devsh DXC fork: dxcmem.cpp calling the Win32-only CoGetMalloc, and the DirectX-Headers bump past upstream's pin whose WSL stubs clash with WinAdapter.h (build against upstream's pinned commit, NBL_DXC_DIRECTX_HEADERS_REF). Co-Authored-By: Claude Opus 5.5 Co-authored-by: multica-agent * NAB-6: MSVC-only PDB install rules and test, ELF install RPATH for executables $ is a generator error with non-MSVC linkers. Executables get $ORIGIN-relative INSTALL_RPATH entries pointing at the packaged Nabla and DXC runtime directories, the ELF counterpart of the Windows delay-load lookup. Co-Authored-By: Claude Opus 5.5 Co-authored-by: multica-agent * NAB-6: link the shared Nabla library on Linux - position independent code for shared builds (static 3rdparty archives go into libNabla.so) - correct libjpeg-turbo archive name/location for non-MSVC and single-config generators - compile volk with -fcommon, its C tentative definitions clash with vulkaninfo's otherwise - link TBB when libstdc++ uses it as the std::execution parallel backend - deduplicate dxcompiler byproducts (import lib == shared object on ELF) - $ORIGIN RPATH on Nabla so the installed library finds DXC Co-Authored-By: Claude Opus 5.5 Co-authored-by: multica-agent --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: multica-agent --- 3rdparty/CMakeLists.txt | 22 +++++- 3rdparty/dxc/CMakeLists.txt | 51 +++++++++++++ CMakeLists.txt | 2 + cmake/common.cmake | 19 ++++- cmake/scripts/linux/init-submodules-cached.sh | 74 +++++++++++++++++++ src/nbl/CMakeLists.txt | 23 +++++- tools/nsc/CMakeLists.txt | 24 +++--- 7 files changed, 199 insertions(+), 16 deletions(-) create mode 100755 cmake/scripts/linux/init-submodules-cached.sh diff --git a/3rdparty/CMakeLists.txt b/3rdparty/CMakeLists.txt index 6e7e9e9970..8e9efa2d7a 100755 --- a/3rdparty/CMakeLists.txt +++ b/3rdparty/CMakeLists.txt @@ -70,6 +70,11 @@ endif() add_subdirectory(volk volk EXCLUDE_FROM_ALL) target_include_directories(volk PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/Vulkan-Headers/include") +# vulkaninfo (C++) defines the same global `vkXxx` function pointers volk.c declares as C tentative definitions, +# MSVC merges them as common symbols, GCC>=10 and Clang>=11 default to -fno-common and the link fails on duplicates +if(NOT MSVC) + target_compile_options(volk PRIVATE -fcommon) +endif() target_include_directories(volk_headers INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/Vulkan-Headers/include") if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") @@ -119,6 +124,10 @@ if(NOT NBL_IS_MULTI_CONFIG) list(APPEND NBL_JPEG_CMAKE_OPTIONS "-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}") endif() +if(CMAKE_POSITION_INDEPENDENT_CODE) + list(APPEND NBL_JPEG_CMAKE_OPTIONS "-DCMAKE_POSITION_INDEPENDENT_CODE:BOOL=ON") +endif() + if(NOT CMAKE_GENERATOR MATCHES "Ninja*") list(APPEND NBL_JPEG_CMAKE_OPTIONS -Ax64) endif() @@ -144,7 +153,18 @@ if(NOT "${JPEG_CMAKE_RESULT}" STREQUAL "0") message(FATAL_ERROR "${JPEG_CMAKE_STREAM_PIPE}") endif() -set(JPEG_LIB_OUTPUT "${THIRD_PARTY_BINARY_DIR}/libjpeg-turbo/$/jpeg-static${CMAKE_STATIC_LIBRARY_SUFFIX}") +# libjpeg-turbo only names the archive `jpeg-static` for MSVC-like compilers (`libjpeg.a` otherwise) +# and single-config generators don't put outputs in a per-config subdirectory +if(MSVC) + set(_NBL_JPEG_LIB_NAME_ "jpeg-static${CMAKE_STATIC_LIBRARY_SUFFIX}") +else() + set(_NBL_JPEG_LIB_NAME_ "${CMAKE_STATIC_LIBRARY_PREFIX}jpeg${CMAKE_STATIC_LIBRARY_SUFFIX}") +endif() +if(NBL_IS_MULTI_CONFIG) + set(JPEG_LIB_OUTPUT "${THIRD_PARTY_BINARY_DIR}/libjpeg-turbo/$/${_NBL_JPEG_LIB_NAME_}") +else() + set(JPEG_LIB_OUTPUT "${THIRD_PARTY_BINARY_DIR}/libjpeg-turbo/${_NBL_JPEG_LIB_NAME_}") +endif() add_custom_command(OUTPUT ${JPEG_LIB_OUTPUT} COMMAND "${CMAKE_COMMAND}" --build "${THIRD_PARTY_BINARY_DIR}/libjpeg-turbo" -t jpeg-static --config $ COMMENT "Building libjpeg-turbo static library" diff --git a/3rdparty/dxc/CMakeLists.txt b/3rdparty/dxc/CMakeLists.txt index 506dd9cc60..ef356d21ff 100644 --- a/3rdparty/dxc/CMakeLists.txt +++ b/3rdparty/dxc/CMakeLists.txt @@ -102,6 +102,57 @@ if(CMAKE_TOOLCHAIN_FILE) list(APPEND NBL_DXC_CMAKE_OPTIONS "-DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}") endif() +# the DXC configure below is a separate CMake invocation, it inherits nothing from this +# cache - forward the toolchain choices explicitly so DXC is built the same way as Nabla +if(NOT MSVC) + list(APPEND NBL_DXC_CMAKE_OPTIONS + "-DCMAKE_C_COMPILER:FILEPATH=${CMAKE_C_COMPILER}" + "-DCMAKE_CXX_COMPILER:FILEPATH=${CMAKE_CXX_COMPILER}" + ) + if(CMAKE_LINKER_TYPE) + list(APPEND NBL_DXC_CMAKE_OPTIONS "-DCMAKE_LINKER_TYPE:STRING=${CMAKE_LINKER_TYPE}") + endif() + # TODO: fix in the DXC fork, lib/DxcSupport/dxcmem.cpp calls the Win32-only CoGetMalloc + # instead of the DxcCoGetMalloc adapter (which maps to CoGetMalloc on Windows anyway) + if(NOT WIN32) + list(APPEND NBL_DXC_CMAKE_OPTIONS "-DCMAKE_CXX_FLAGS:STRING=$ENV{CXXFLAGS} -DCoGetMalloc=DxcCoGetMalloc") + + # TODO: fix in the DXC fork, its merge of upstream main bumped external/DirectX-Headers past the + # commit upstream DXC pins, the newer wsl/stubs/basetsd.h redefines the WinAdapter.h typedefs. + # Until then export the upstream-pinned headers (an ancestor of the fork's pin) and use those. + set(NBL_DXC_DIRECTX_HEADERS_REF 980971e835876dc0cde415e8f9bc646e64667bf7 CACHE STRING "DirectX-Headers commit DXC is built against on non-Windows hosts, empty = the submodule checkout") + if(NBL_DXC_DIRECTX_HEADERS_REF) + find_package(Git REQUIRED) + set(_NBL_DXC_DX_HEADERS_SRC_ "${CMAKE_CURRENT_SOURCE_DIR}/dxc/external/DirectX-Headers") + set(_NBL_DXC_DX_HEADERS_DIR_ "${CMAKE_CURRENT_BINARY_DIR}/DirectX-Headers-${NBL_DXC_DIRECTX_HEADERS_REF}") + if(NOT EXISTS "${_NBL_DXC_DX_HEADERS_DIR_}/include/directx/d3d12.h") + file(MAKE_DIRECTORY "${_NBL_DXC_DX_HEADERS_DIR_}") + execute_process(COMMAND "${GIT_EXECUTABLE}" -C "${_NBL_DXC_DX_HEADERS_SRC_}" archive --format=tar -o "${_NBL_DXC_DX_HEADERS_DIR_}.tar" "${NBL_DXC_DIRECTX_HEADERS_REF}" include + RESULT_VARIABLE _NBL_RES_ ERROR_VARIABLE _NBL_ERR_ + ) + if(NOT _NBL_RES_ EQUAL 0) + message(FATAL_ERROR "Could not export DirectX-Headers @ ${NBL_DXC_DIRECTX_HEADERS_REF} (shallow submodule?): ${_NBL_ERR_}") + endif() + file(ARCHIVE_EXTRACT INPUT "${_NBL_DXC_DX_HEADERS_DIR_}.tar" DESTINATION "${_NBL_DXC_DX_HEADERS_DIR_}") + file(REMOVE "${_NBL_DXC_DX_HEADERS_DIR_}.tar") + endif() + list(APPEND NBL_DXC_CMAKE_OPTIONS "-DDIRECTX_HEADER_INCLUDE_DIR:PATH=${_NBL_DXC_DX_HEADERS_DIR_}/include") + endif() + endif() + if(USE_CCACHE) + list(APPEND NBL_DXC_CMAKE_OPTIONS + "-DCMAKE_C_COMPILER_LAUNCHER:STRING=${CCACHE_FOUND}" + "-DCMAKE_CXX_COMPILER_LAUNCHER:STRING=${CCACHE_FOUND}" + ) + endif() +endif() + +# LLVM links are the memory peak of the whole build, by default allow one at a time +set(NBL_DXC_PARALLEL_LINK_JOBS 1 CACHE STRING "LLVM_PARALLEL_LINK_JOBS for the DXC build (Ninja only, empty = unlimited)") +if(CMAKE_GENERATOR MATCHES "Ninja" AND NBL_DXC_PARALLEL_LINK_JOBS) + list(APPEND NBL_DXC_CMAKE_OPTIONS "-DLLVM_PARALLEL_LINK_JOBS:STRING=${NBL_DXC_PARALLEL_LINK_JOBS}") +endif() + # TODO: add_subdirectory but don't modify dxc target compile options execute_process(COMMAND "${CMAKE_COMMAND}" -C "${CMAKE_CURRENT_SOURCE_DIR}/dxc/cmake/caches/PredefinedParams.cmake" -S "${CMAKE_CURRENT_SOURCE_DIR}/dxc" -B "${DXC_BUILD_DIR}" -G "${CMAKE_GENERATOR}" ${NBL_DXC_CMAKE_OPTIONS} RESULT_VARIABLE DXC_CMAKE_RESULT diff --git a/CMakeLists.txt b/CMakeLists.txt index ee888eb868..f1ff271c18 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -60,6 +60,8 @@ else() if(NOT NBL_COMPILER_DYNAMIC_RUNTIME) message(FATAL_ERROR "Turn NBL_COMPILER_DYNAMIC_RUNTIME on! For dynamic Nabla builds dynamic runtime is mandatory!") endif() + # static 3rdparty archives get linked into the Nabla shared object, ELF needs them position independent (no-op for PE/COFF) + set(CMAKE_POSITION_INDEPENDENT_CODE ON) endif() find_package(Vulkan) diff --git a/cmake/common.cmake b/cmake/common.cmake index 234698fc3d..ebafb7d2bc 100755 --- a/cmake/common.cmake +++ b/cmake/common.cmake @@ -195,6 +195,13 @@ macro(nbl_create_executable_project _EXTRA_SOURCES _EXTRA_OPTIONS _EXTRA_INCLUDE PRIVATE "-DNBL_CPACK_PACKAGE_NABLA_DLL_DIR=\"${_NBL_NABLA_PACKAGE_RUNTIME_DLL_DIR_PATH_REL_TO_TARGET_}\"" PRIVATE "-DNBL_CPACK_PACKAGE_DXC_DLL_DIR=\"${_NBL_DXC_PACKAGE_RUNTIME_DLL_DIR_PATH_REL_TO_TARGET_}\"" ) + # ELF has no delay loading, the installed executable finds Nabla and DXC through its RUNPATH + if(UNIX AND NOT APPLE AND NOT ANDROID) + set_property(TARGET ${EXECUTABLE_NAME} APPEND PROPERTY INSTALL_RPATH + "$ORIGIN/${_NBL_NABLA_PACKAGE_RUNTIME_DLL_DIR_PATH_REL_TO_TARGET_}" + "$ORIGIN/${_NBL_DXC_PACKAGE_RUNTIME_DLL_DIR_PATH_REL_TO_TARGET_}" + ) + endif() endif() nbl_project_process_test_module() @@ -301,7 +308,9 @@ function(nbl_install_program_spec _TRGT _RELATIVE_DESTINATION) install(PROGRAMS $ DESTINATION ${_DEST_GE_} CONFIGURATIONS ${_CONFIGURATION_} COMPONENT Runtimes) endforeach() - install(PROGRAMS $ DESTINATION debug/runtime/${_RELATIVE_DESTINATION} CONFIGURATIONS Debug COMPONENT Runtimes) # TODO: write cmake script with GE to detect if target in configuration has PDB files generated then add install rule + if(MSVC) # PDBs only exist with MSVC-style linkers, $ is an error elsewhere + install(PROGRAMS $ DESTINATION debug/runtime/${_RELATIVE_DESTINATION} CONFIGURATIONS Debug COMPONENT Runtimes) # TODO: write cmake script with GE to detect if target in configuration has PDB files generated then add install rule + endif() get_property(_DEFINED_PROPERTY_ TARGET ${_TRGT} @@ -362,9 +371,11 @@ function(nbl_install_exe_spec _TARGETS _RELATIVE_DESTINATION) install(TARGETS ${_TARGETS} ${_EXPORT_ARGS} RUNTIME DESTINATION ${_DEST_GE_} COMPONENT ${_COMPONENT}) - foreach(_TRGT IN LISTS _TARGETS) - install(PROGRAMS $ DESTINATION debug/exe/${_RELATIVE_DESTINATION} CONFIGURATIONS Debug COMPONENT ${_COMPONENT}) - endforeach() + if(MSVC) # PDBs only exist with MSVC-style linkers + foreach(_TRGT IN LISTS _TARGETS) + install(PROGRAMS $ DESTINATION debug/exe/${_RELATIVE_DESTINATION} CONFIGURATIONS Debug COMPONENT ${_COMPONENT}) + endforeach() + endif() foreach(_TRGT IN LISTS _TARGETS) get_property(_DEFINED_PROPERTY_ diff --git a/cmake/scripts/linux/init-submodules-cached.sh b/cmake/scripts/linux/init-submodules-cached.sh new file mode 100755 index 0000000000..7bb3e39110 --- /dev/null +++ b/cmake/scripts/linux/init-submodules-cached.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# Hydrate the submodules of a Nabla checkout from an already hydrated local +# clone (the "cache") instead of downloading them again from GitHub. +# +# Usage: init-submodules-cached.sh [] +# +# For every submodule, recursively, the URL is overridden in the target's local +# git config (never in .gitmodules) to point at the matching submodule work +# tree inside the cache, then `git submodule update --init` clones it. A local +# clone hard-links objects, so the target does not depend on the cache's object +# store afterwards (no alternates, safe if the cache is ever repacked). +# +# Submodules that are not hydrated in the cache are skipped: these are the ones +# excluded by policy (`update = none`, private repositories). Run with +# NBL_UPDATE_GIT_SUBMODULE=OFF afterwards so CMake does not try to update them. + +set -euo pipefail + +if [[ $# -lt 1 ]]; then + echo "usage: $0 []" >&2 + exit 2 +fi + +CACHE_ROOT="$(cd "$1" && pwd)" +TARGET_ROOT="$(cd "${2:-.}" && git rev-parse --show-toplevel)" + +rel() { [[ "$1" == "$TARGET_ROOT" ]] && echo "" || echo "${1#$TARGET_ROOT/}/"; } + +hydrate() { + local target="$1" cache="$2" + [[ -f "$target/.gitmodules" ]] || return 0 + + local key name path + while read -r key path; do + name="${key#submodule.}" + name="${name%.path}" + + if [[ ! -e "$cache/$path/.git" ]] || ! git -C "$cache/$path" rev-parse --verify -q HEAD >/dev/null; then + echo "skip $(rel "$target")$path (not hydrated in cache)" + continue + fi + if [[ "$(git -C "$cache/$path" rev-parse --show-toplevel)" != "$cache/$path" ]]; then + echo "skip $(rel "$target")$path (cache path is not a checkout)" + continue + fi + + echo "init $(rel "$target")$path" + git -C "$target" config "submodule.$name.url" "$cache/$path" + git -C "$target" -c protocol.file.allow=always submodule update --init -- "$path" + + # carry over cache-local attribute fixes (e.g. line-ending overrides) + local cache_attr target_attr + cache_attr="$(git -C "$cache/$path" rev-parse --git-path info/attributes)" + target_attr="$(git -C "$target/$path" rev-parse --git-path info/attributes)" + [[ "$cache_attr" = /* ]] || cache_attr="$cache/$path/$cache_attr" + [[ "$target_attr" = /* ]] || target_attr="$target/$path/$target_attr" + if [[ -s "$cache_attr" ]]; then + mkdir -p "$(dirname "$target_attr")" + cp "$cache_attr" "$target_attr" + fi + + hydrate "$target/$path" "$cache/$path" + done < <(git -C "$target" config -f .gitmodules --get-regexp '^submodule\..*\.path$' || true) +} + +hydrate "$TARGET_ROOT" "$CACHE_ROOT" + +# A matching SHA is not proof of a populated work tree (see NAB-5): verify. +dirty="$(git -C "$TARGET_ROOT" submodule foreach --quiet --recursive 'git status --porcelain' | wc -l)" +if [[ "$dirty" != "0" ]]; then + echo "error: $dirty dirty entries in submodule work trees" >&2 + exit 1 +fi +echo "ok: submodules hydrated and clean" diff --git a/src/nbl/CMakeLists.txt b/src/nbl/CMakeLists.txt index 2abcb329f7..139da9d8bc 100644 --- a/src/nbl/CMakeLists.txt +++ b/src/nbl/CMakeLists.txt @@ -634,7 +634,7 @@ endif() # TODO: remove and replace with target in tree add_custom_target(dxcompiler COMMAND "${CMAKE_COMMAND}" --build "${DXC_BUILD_DIR}" --target dxcompiler --config $,Debug,Release> -j${_NBL_JOBS_AMOUNT_} - BYPRODUCTS "${DXC_LIB}" "${DXC_DLL}" + BYPRODUCTS "$" # on ELF platforms the import library and the shared object are the same file VERBATIM COMMAND_EXPAND_LISTS ) @@ -756,6 +756,20 @@ list(APPEND INTERFACE_BUILD_DEFINITIONS # TODO: private target_link_libraries(Nabla PUBLIC $) +# libstdc++ implements the parallel `std::execution` policies on top of oneTBB whenever is found, +# then everything instantiating them (Nabla and its consumers) has to link TBB +if(CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "GNU" AND NOT ANDROID AND NOT APPLE) + include(CheckCXXSourceCompiles) + block() + set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) # compile only, the point is that it won't link without TBB + check_cxx_source_compiles("#if !__has_include()\n#error no TBB\n#endif\nint main() {return 0;}" NBL_STDLIB_USES_TBB_PAR_BACKEND) + endblock() + if(NBL_STDLIB_USES_TBB_PAR_BACKEND) + find_package(TBB REQUIRED CONFIG) + target_link_libraries(Nabla PUBLIC TBB::tbb) + endif() +endif() + # vulkaninfo if (NBL_STATIC_BUILD) target_link_libraries(Nabla PUBLIC $) @@ -905,6 +919,13 @@ nbl_install_dir_spec(../../include/nbl/application_templates nbl) # note: order important, keep after install rules due to NBL_3RDPARTY_DXC_NS_PACKAGE_RUNTIME_DLL_DIR_PATH property get_property(_NBL_DXC_PACKAGE_RUNTIME_DLL_DIR_PATH_ GLOBAL PROPERTY NBL_3RDPARTY_DXC_NS_PACKAGE_RUNTIME_DLL_DIR_PATH) get_target_property(_NBL_NABLA_PACKAGE_RUNTIME_DLL_DIR_PATH_ Nabla NBL_PACKAGE_RUNTIME_DLL_DIR_PATH) +# the installed Nabla shared object must find DXC next to it (an executable's RUNPATH doesn't apply to its dependencies), +# it gets installed as a plain file copy of the build output so the entry goes into the build RPATH +if(UNIX AND NOT APPLE AND NOT ANDROID AND NOT NBL_STATIC_BUILD) + cmake_path(RELATIVE_PATH _NBL_DXC_PACKAGE_RUNTIME_DLL_DIR_PATH_ BASE_DIRECTORY "${_NBL_NABLA_PACKAGE_RUNTIME_DLL_DIR_PATH_}" OUTPUT_VARIABLE _NBL_DXC_DIR_REL_TO_NABLA_) + set_property(TARGET Nabla APPEND PROPERTY BUILD_RPATH "$ORIGIN/${_NBL_DXC_DIR_REL_TO_NABLA_}") + set_property(TARGET Nabla APPEND PROPERTY INSTALL_RPATH "$ORIGIN/${_NBL_DXC_DIR_REL_TO_NABLA_}") +endif() set(_NBL_CONSUMER_BIN_DIR_GE_ "$,$>>" ) diff --git a/tools/nsc/CMakeLists.txt b/tools/nsc/CMakeLists.txt index e8549eb281..7949ae072e 100644 --- a/tools/nsc/CMakeLists.txt +++ b/tools/nsc/CMakeLists.txt @@ -271,19 +271,23 @@ set(NBL_NSC_COMPILE_COMMAND set(NBL_NSC_PREINSTALL_TARGET_EXE_DIRECTORY "${NBL_NSC_PREINSTALL_DIRECTORY}/${NBL_PACKAGE_RUNTIME_EXE_DIR_PATH}") set(NBL_NSC_PREINSTALL_TARGET_EXE_FILENAME $) set(NBL_NSC_PREINSTALL_TARGET_EXE_FILEPATH "${NBL_NSC_PREINSTALL_TARGET_EXE_DIRECTORY}/${NBL_NSC_PREINSTALL_TARGET_EXE_FILENAME}") -set(NBL_NSC_PREINSTALL_TARGET_PDB_FILENAME $) -set(NBL_NSC_PREINSTALL_TARGET_PDB_FILEPATH "${NBL_NSC_PREINSTALL_TARGET_EXE_DIRECTORY}/${NBL_NSC_PREINSTALL_TARGET_PDB_FILENAME}") set(NBL_NSC_BUILD_INFO_FILENAME build-info.json) set(NBL_NSC_PREINSTALL_TARGET_BUILD_INFO "${NBL_NSC_PREINSTALL_TARGET_EXE_DIRECTORY}/${NBL_NSC_BUILD_INFO_FILENAME}") -add_test(NAME NBL_NSC_INSTALL_EXECUTABLES_PDB_TEST - COMMAND "${CMAKE_COMMAND}" - -DNBL_CONFIG=$ - -DNBL_PDB_FILEPATH=${NBL_NSC_PREINSTALL_TARGET_PDB_FILEPATH} - -P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/VerifyInstalledExecutablePdb.cmake" - COMMAND_EXPAND_LISTS -) -set_tests_properties(NBL_NSC_INSTALL_EXECUTABLES_PDB_TEST PROPERTIES DEPENDS NBL_NSC_INSTALL_EXECUTABLES_TEST) +# PDBs only exist with MSVC-style linkers, $ is an error elsewhere +if(MSVC) + set(NBL_NSC_PREINSTALL_TARGET_PDB_FILENAME $) + set(NBL_NSC_PREINSTALL_TARGET_PDB_FILEPATH "${NBL_NSC_PREINSTALL_TARGET_EXE_DIRECTORY}/${NBL_NSC_PREINSTALL_TARGET_PDB_FILENAME}") + + add_test(NAME NBL_NSC_INSTALL_EXECUTABLES_PDB_TEST + COMMAND "${CMAKE_COMMAND}" + -DNBL_CONFIG=$ + -DNBL_PDB_FILEPATH=${NBL_NSC_PREINSTALL_TARGET_PDB_FILEPATH} + -P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/VerifyInstalledExecutablePdb.cmake" + COMMAND_EXPAND_LISTS + ) + set_tests_properties(NBL_NSC_INSTALL_EXECUTABLES_PDB_TEST PROPERTIES DEPENDS NBL_NSC_INSTALL_EXECUTABLES_TEST) +endif() add_test(NAME NBL_NSC_COMPILE_AT_EXE_CWD_TEST COMMAND "${NBL_NSC_PREINSTALL_TARGET_EXE_FILENAME}" ${NBL_NSC_COMPILE_COMMAND} From 0f48dfc7db5f7e83481641ee59e687b29633860b Mon Sep 17 00:00:00 2001 From: Raydelto Hernandez Date: Wed, 7 Oct 2026 16:00:55 -0300 Subject: [PATCH 2/9] NAB-6: POSIX core, HLSL compiler and Clang conformance fixes (2/2, stacked) (#2) * NAB-6: add cached submodule hydration helper for Linux worktrees Clones every submodule (recursively) from an already hydrated local checkout instead of GitHub, carries over cache-local attribute overrides and verifies the work trees are actually populated. Co-Authored-By: Claude Opus 5.5 Co-authored-by: multica-agent * NAB-6: forward toolchain to the DXC sub-build, work around DXC fork Linux breakage The DXC configure is a separate CMake invocation, so forward compiler, linker type, ccache launcher and LLVM_PARALLEL_LINK_JOBS (new NBL_DXC_PARALLEL_LINK_JOBS, default 1) explicitly. On non-Windows hosts also work around two problems in the Devsh DXC fork: dxcmem.cpp calling the Win32-only CoGetMalloc, and the DirectX-Headers bump past upstream's pin whose WSL stubs clash with WinAdapter.h (build against upstream's pinned commit, NBL_DXC_DIRECTX_HEADERS_REF). Co-Authored-By: Claude Opus 5.5 Co-authored-by: multica-agent * NAB-6: MSVC-only PDB install rules and test, ELF install RPATH for executables $ is a generator error with non-MSVC linkers. Executables get $ORIGIN-relative INSTALL_RPATH entries pointing at the packaged Nabla and DXC runtime directories, the ELF counterpart of the Windows delay-load lookup. Co-Authored-By: Claude Opus 5.5 Co-authored-by: multica-agent * NAB-6: link the shared Nabla library on Linux - position independent code for shared builds (static 3rdparty archives go into libNabla.so) - correct libjpeg-turbo archive name/location for non-MSVC and single-config generators - compile volk with -fcommon, its C tentative definitions clash with vulkaninfo's otherwise - link TBB when libstdc++ uses it as the std::execution parallel backend - deduplicate dxcompiler byproducts (import lib == shared object on ELF) - $ORIGIN RPATH on Nabla so the installed library finds DXC Co-Authored-By: Claude Opus 5.5 Co-authored-by: multica-agent * NAB-6: POSIX/Linux core system layer - IApplicationFramework::createSystem() returns a CSystemLinux on Linux - GlobalsInit() verifies dxcompiler/Nabla are mapped (RUNPATH) or dlopens them from the lookup paths - drop the broken duplicate isDebuggerAttached() from CSystemLinux.cpp, harden the ISystem one against read() errors - RenderDoc: stop shadowing RENDERDOC_GetAPI on Linux/Android so the API actually gets hooked - external handles: return ExternalHandleNull (fd -1 on POSIX) instead of nullptr - CCUDAHandler: include ModuleLookupUtils.h for executableDirectory() Co-Authored-By: Claude Opus 5.5 Co-authored-by: multica-agent * NAB-6: enable the DXC based HLSL compiler on Linux DXC exposes the same dxcapi on Linux through its WinAdapter, the only missing piece is WRL's ComPtr, shimmed on top of DXC's CComPtr. Needed by nsc. Co-Authored-By: Claude Opus 5.5 Co-authored-by: multica-agent * NAB-6: C++ conformance fixes so Nabla compiles with GNU-frontend Clang MSVC accepts these, Clang 20 (libstdc++) does not: - missing template/typename disambiguators, stray template keywords - default arguments / container instantiations needing a nested class' default member initializers before the enclosing class is complete (Clang silently invalidates code, e.g. CAssetConverter hash maps and SConvReqAccelerationStructure) - stale code in uninstantiated templates (BLAS tracking after 82e3ae118, TLAS instance std::get) - member template specializations outside their namespace, duplicate explicit instantiations, access mismatches between declarations and definitions, duplicate alias, size_t != uint64_t - GLM swizzle operators need GLM to see MS extensions, provided only around its configuration - case-sensitive include (Imath), missing includes (chrono, memory_resource, string_view) Co-Authored-By: Claude Opus 5.5 Co-authored-by: multica-agent --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: multica-agent --- include/nbl/asset/IAccelerationStructure.h | 1 - include/nbl/asset/IAnimationLibrary.h | 2 +- include/nbl/asset/ICPUGeometryCollection.h | 2 +- include/nbl/asset/IGeometry.h | 9 ++--- include/nbl/asset/filters/CBlitImageFilter.h | 4 +-- include/nbl/asset/filters/CBlitUtilities.h | 4 +-- .../filters/CSwizzleAndConvertImageFilter.h | 10 +++--- .../kernels/CConvolutionWeightFunction.h | 10 +++--- .../asset/filters/kernels/WeightFunctions.h | 4 +-- .../nbl/asset/material_compiler3/CTrueIR.h | 20 ++++++----- include/nbl/asset/utils/CCompilerSet.h | 6 ++-- include/nbl/asset/utils/CDirQuantCacheBase.h | 2 +- include/nbl/asset/utils/CGeometryCreator.h | 4 ++- include/nbl/asset/utils/CHLSLCompiler.h | 2 +- include/nbl/asset/utils/CVertexHashGrid.h | 2 +- include/nbl/asset/utils/IShaderCompiler.h | 6 ++-- .../hlsl/cpp_compat/impl/intrinsics_impl.hlsl | 6 ++-- .../nbl/builtin/hlsl/cpp_compat/vector.hlsl | 14 ++++++++ .../nbl/builtin/hlsl/format/shared_exp.hlsl | 2 +- include/nbl/builtin/hlsl/limits.hlsl | 2 +- .../core/alloc/SimpleBlockBasedAllocator.h | 6 ++-- .../nbl/core/alloc/refctd_memory_resource.h | 3 ++ include/nbl/core/containers/CMemoryPool.h | 6 ++-- include/nbl/core/containers/CObjectPool.h | 4 +-- .../nbl/core/containers/DoublyLinkedList.h | 2 +- include/nbl/core/hash/blake.h | 6 ++-- include/nbl/system/IApplicationFramework.h | 32 +++++++++++++++-- include/nbl/system/IFileBase.h | 1 + include/nbl/system/to_string.h | 2 +- include/nbl/video/IDeviceMemoryAllocation.h | 4 ++- include/nbl/video/IGPUAccelerationStructure.h | 16 +++++++-- include/nbl/video/IGPUCommandPool.h | 6 ++++ include/nbl/video/ILogicalDevice.h | 11 +++--- include/nbl/video/IPhysicalDevice.h | 2 +- include/nbl/video/utilities/CAssetConverter.h | 20 +++++++---- include/nbl/video/utilities/CComputeBlit.h | 1 + include/nbl/video/utilities/IUtilities.h | 2 +- src/nbl/asset/ICPUPolygonGeometry.cpp | 4 +-- .../asset/material_compiler3/CFrontendIR.cpp | 2 +- src/nbl/asset/material_compiler3/CTrueIR.cpp | 2 +- src/nbl/asset/utils/CCompilerSet.cpp | 4 +-- src/nbl/asset/utils/CHLSLCompiler.cpp | 26 ++++++++++---- src/nbl/core/alloc/refctd_memory_resource.cpp | 1 + src/nbl/system/CSystemLinux.cpp | 35 ------------------- src/nbl/system/ISystem.cpp | 7 ++-- src/nbl/video/CCUDAHandler.cpp | 1 + src/nbl/video/CVulkanAccelerationStructure.h | 2 +- src/nbl/video/CVulkanMemoryAllocation.cpp | 4 +-- src/nbl/video/CVulkanSemaphore.cpp | 4 +-- src/nbl/video/IAPIConnection.cpp | 4 +-- src/nbl/video/ILogicalDevice.cpp | 7 +++- src/nbl/video/utilities/CAssetConverter.cpp | 4 +-- 52 files changed, 207 insertions(+), 136 deletions(-) diff --git a/include/nbl/asset/IAccelerationStructure.h b/include/nbl/asset/IAccelerationStructure.h index a1d325a2a3..2f51462067 100644 --- a/include/nbl/asset/IAccelerationStructure.h +++ b/include/nbl/asset/IAccelerationStructure.h @@ -158,7 +158,6 @@ class IBottomLevelAccelerationStructure : public IAccelerationStructure using AABB_t = core::aabbox3d; protected: - using base_build_flags_t = IAccelerationStructure::BUILD_FLAGS; using IAccelerationStructure::IAccelerationStructure; virtual ~IBottomLevelAccelerationStructure() = default; diff --git a/include/nbl/asset/IAnimationLibrary.h b/include/nbl/asset/IAnimationLibrary.h index 3ab87e5d32..ff398a043c 100644 --- a/include/nbl/asset/IAnimationLibrary.h +++ b/include/nbl/asset/IAnimationLibrary.h @@ -32,7 +32,7 @@ class IAnimationLibrary : public virtual core::IReferenceCounted Keyframe() : scale(0ull) // TODO: initialize scale to 1.f { translation[2] = translation[1] = translation[0] = 0.f; - quat = core::vectorSIMDu32(128u,128u,128u,255u); // should be (0,0,0,1) encoded + quat = hlsl::uint32_t4(128u,128u,128u,255u); // should be (0,0,0,1) encoded } Keyframe(const core::vectorSIMDf& _scale, const hlsl::math::quaternion& _quat, const CQuantQuaternionCache* quantCache, const core::vectorSIMDf& _translation) { diff --git a/include/nbl/asset/ICPUGeometryCollection.h b/include/nbl/asset/ICPUGeometryCollection.h index 9e84de833e..6c3d52300a 100644 --- a/include/nbl/asset/ICPUGeometryCollection.h +++ b/include/nbl/asset/ICPUGeometryCollection.h @@ -55,7 +55,7 @@ class NBL_API2 ICPUGeometryCollection : public IAsset, public IGeometryCollectio } // - inline bool setAABB(const decltype(base_t::m_aabb)& aabb) + inline bool setAABB(const decltype(m_aabb)& aabb) { if (isMutable()) { diff --git a/include/nbl/asset/IGeometry.h b/include/nbl/asset/IGeometry.h index 319c98d99e..b70f23c0e2 100644 --- a/include/nbl/asset/IGeometry.h +++ b/include/nbl/asset/IGeometry.h @@ -363,7 +363,7 @@ class IGeometry : public std::conditional_t requires (hlsl::concepts::Vector && std::is_same_v && std::is_same_v) - inline void encodeElement(const Index elIx, const V& v) + inline bool encodeElement(const Index elIx, const V& v) { if (!composed.isFormatted()) return false; @@ -417,14 +417,15 @@ class IGeometry : public std::conditional_tgetPointer(elIx); + return _this->template getPointer(elIx); return nullptr; } template requires hlsl::concepts::Vector - inline void encodeElement(const Index elIx, const V& v) + inline bool encodeElement(const Index elIx, const V& v) { if (_this) - _this->encodeElement(elIx,v); + return _this->template encodeElement(elIx,v); + return false; } }; // diff --git a/include/nbl/asset/filters/CBlitImageFilter.h b/include/nbl/asset/filters/CBlitImageFilter.h index f228fea325..86db234a53 100644 --- a/include/nbl/asset/filters/CBlitImageFilter.h +++ b/include/nbl/asset/filters/CBlitImageFilter.h @@ -539,7 +539,7 @@ class CBlitImageFilter : auto sample = lineBuffer+i*ChannelCount; - base_t::template onDecode(inFormat, state, srcPix, sample, blockLocalTexelCoord.x, blockLocalTexelCoord.y, ChannelCount); + base_t::onDecode(inFormat, state, srcPix, sample, blockLocalTexelCoord.x, blockLocalTexelCoord.y, ChannelCount); if (nonPremultBlendSemantic) { @@ -610,7 +610,7 @@ class CBlitImageFilter : // we'll only get here if we have to do coverage adjustment if (needsNormalization && lastPass) { - state->normalization.finalize(); + state->normalization.template finalize(); storeToImage(core::rational(cvg_num,cvg_den),axis,outOffsetLayer); } }; diff --git a/include/nbl/asset/filters/CBlitUtilities.h b/include/nbl/asset/filters/CBlitUtilities.h index 51669662ba..c61c3efe50 100644 --- a/include/nbl/asset/filters/CBlitUtilities.h +++ b/include/nbl/asset/filters/CBlitUtilities.h @@ -91,8 +91,8 @@ class CBlitUtilities : public IBlitUtilities using convolution_kernel_y_t = std::tuple_element_t<1, convolution_kernels_t>; using convolution_kernel_z_t = std::tuple_element_t<2, convolution_kernels_t>; - using value_type = convolution_kernel_x_t::value_t; - static_assert(std::is_same_v && std::is_same_v); + using value_type = typename convolution_kernel_x_t::value_t; + static_assert(std::is_same_v && std::is_same_v); using lut_value_type = LutDataType; static_assert(std::is_same_v || std::is_same_v, "Invalid LUT data type."); diff --git a/include/nbl/asset/filters/CSwizzleAndConvertImageFilter.h b/include/nbl/asset/filters/CSwizzleAndConvertImageFilter.h index 70a569aac9..7827bead1b 100644 --- a/include/nbl/asset/filters/CSwizzleAndConvertImageFilter.h +++ b/include/nbl/asset/filters/CSwizzleAndConvertImageFilter.h @@ -82,7 +82,7 @@ class CSwizzleAndConvertImageFilterBase : public CSwizzleableAndDitherableFilter return true; }; CMatchedSizeInOutImageFilterCommon::commonExecute(state,perOutputRegion); - state->normalization.finalize(); + state->normalization.template finalize(); } } }; @@ -216,8 +216,8 @@ class CSwizzleAndConvertImageFilter(state, dstPix, decodeBuffer, localOutPos, blockX, blockY, outChannelsAmount); } }; @@ -366,7 +366,7 @@ class CSwizzleAndConvertImageFilter(state, srcPix, decodeBuffer, blockX, blockY, maxChannels); - base_t::template onEncode(outFormat, state, dstPix, decodeBuffer, localOutPos, blockX, blockY, outChannelsAmount); + base_t::onEncode(outFormat, state, dstPix, decodeBuffer, localOutPos, blockX, blockY, outChannelsAmount); } }; CBasicImageFilterCommon::executePerRegion(policy, commonExecuteData.inImg, swizzle, commonExecuteData.inRegions, clip); diff --git a/include/nbl/asset/filters/kernels/CConvolutionWeightFunction.h b/include/nbl/asset/filters/kernels/CConvolutionWeightFunction.h index 84eb757cd1..382d7d8d41 100644 --- a/include/nbl/asset/filters/kernels/CConvolutionWeightFunction.h +++ b/include/nbl/asset/filters/kernels/CConvolutionWeightFunction.h @@ -21,13 +21,13 @@ namespace nbl::asset template class CConvolutionWeightFunction1D final : public impl::IWeightFunction1D { - static_assert(std::is_same_v, "Both functions must use the same Value Type!"); + static_assert(std::is_same_v, "Both functions must use the same Value Type!"); public: - using value_t = WeightFunction1DA::value_t; + using value_t = typename WeightFunction1DA::value_t; constexpr static inline uint32_t k_smoothness = WeightFunction1DA::k_smoothness + WeightFunction1DB::k_smoothness; inline CConvolutionWeightFunction1D(WeightFunction1DA&& funcA, WeightFunction1DB&& funcB) - : impl::IWeightFunction1D(funcA.getMinSupport()+funcB.getMinSupport(), funcA.getMaxSupport()+funcB.getMaxSupport()), m_funcA(std::move(funcA)), m_funcB(std::move(funcB)) + : impl::IWeightFunction1D(funcA.getMinSupport()+funcB.getMinSupport(), funcA.getMaxSupport()+funcB.getMaxSupport()), m_funcA(std::move(funcA)), m_funcB(std::move(funcB)) { } @@ -45,9 +45,9 @@ class CConvolutionWeightFunction1D final : public impl::IWeightFunction1DgetInvStretch(); - if constexpr (std::is_same_v && WeightFunction1DB::k_derivative==0) + if constexpr (std::is_same_v && WeightFunction1DB::k_derivative==0) retval = m_funcA.weight(x); - else if (std::is_same_v && WeightFunction1DA::k_derivative==0) + else if (std::is_same_v && WeightFunction1DA::k_derivative==0) retval = m_funcB.weight(x); else { diff --git a/include/nbl/asset/filters/kernels/WeightFunctions.h b/include/nbl/asset/filters/kernels/WeightFunctions.h index af2782dfac..4168f1352b 100644 --- a/include/nbl/asset/filters/kernels/WeightFunctions.h +++ b/include/nbl/asset/filters/kernels/WeightFunctions.h @@ -346,9 +346,9 @@ class CWeightFunction1D final : public impl::IWeightFunction1Dk_derivative) + if constexpr(sizeof(function_t::k_energy)/sizeof(typename base_t::value_t)>k_derivative) { // normally it would be `scale*invStretch^(derivative+1)*k_energy[k_derivative]` // but `scale` already contains precomputed `invStretch^derivative` factor when we call `stretch` diff --git a/include/nbl/asset/material_compiler3/CTrueIR.h b/include/nbl/asset/material_compiler3/CTrueIR.h index d9a65e8719..f1da0ddfb1 100644 --- a/include/nbl/asset/material_compiler3/CTrueIR.h +++ b/include/nbl/asset/material_compiler3/CTrueIR.h @@ -332,7 +332,7 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // which factors get `1-x` for an Add node or `-x` for a Mul node before getting used uint64_t childIxComplementMask : 57 = 0x0u; }; - static_assert(sizeof(SState) == sizeof(IFactor::padding)); + static_assert(sizeof(SState) == sizeof(padding)); // static inline uint32_t calc_size(const SState state) @@ -1503,9 +1503,9 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! private: template - inline typed_pointer_type emplace(FuncArgs&&... args) + inline typed_pointer_type emplace(FuncArgs&&... funcArgs) { - const auto retval = args.dst->getObjectPool().emplace(1u,std::forward(args)...); + const auto retval = args.dst->getObjectPool().template emplace(1u,std::forward(funcArgs)...); if (retval) createdNodes.push_back(retval); return retval; @@ -1661,9 +1661,14 @@ inline void CTrueIR::SBasicNDFParams::printDot(std::ostringstream& sstr, const c } } +} // namespace nbl::asset::material_compiler3 + +// specializations of a member template have to live in the namespace enclosing the class +namespace nbl::core +{ // specialization of parameter hashing template -struct core::blake3_hasher::update_impl +struct blake3_hasher::update_impl { using input_t = asset::material_compiler3::CTrueIR::SParameter; @@ -1714,7 +1719,7 @@ struct core::blake3_hasher::update_impl } }; template -struct core::blake3_hasher::update_impl,Dummy> +struct blake3_hasher::update_impl,Dummy> { using input_t = asset::material_compiler3::CTrueIR::SParameterSet; @@ -1735,7 +1740,7 @@ struct core::blake3_hasher::update_impl,Dummy> } }; template -struct core::blake3_hasher::update_impl +struct blake3_hasher::update_impl { using input_t = asset::material_compiler3::CTrueIR::SBasicNDFParams; @@ -1751,7 +1756,6 @@ struct core::blake3_hasher::update_impl hasher << input.reference; } }; - -} // namespace nbl::asset::material_compiler3 +} // namespace nbl::core #endif diff --git a/include/nbl/asset/utils/CCompilerSet.h b/include/nbl/asset/utils/CCompilerSet.h index 244f465f78..76786704f4 100644 --- a/include/nbl/asset/utils/CCompilerSet.h +++ b/include/nbl/asset/utils/CCompilerSet.h @@ -15,7 +15,7 @@ class NBL_API2 CCompilerSet : public core::IReferenceCounted public: CCompilerSet(core::smart_refctd_ptr&& sys) : -#ifdef _NBL_PLATFORM_WINDOWS_ +#if defined(_NBL_PLATFORM_WINDOWS_) || defined(_NBL_PLATFORM_LINUX_) // DXC is available on both m_HLSLCompiler(core::make_smart_refctd_ptr(core::smart_refctd_ptr(sys))), #endif m_GLSLCompiler(core::make_smart_refctd_ptr(core::smart_refctd_ptr(sys))) @@ -29,7 +29,7 @@ class NBL_API2 CCompilerSet : public core::IReferenceCounted { if (contentType==IShader::E_CONTENT_TYPE::ECT_HLSL) { -#ifdef _NBL_PLATFORM_WINDOWS_ +#if defined(_NBL_PLATFORM_WINDOWS_) || defined(_NBL_PLATFORM_LINUX_) // DXC is available on both return m_HLSLCompiler; #else return nullptr; @@ -42,7 +42,7 @@ class NBL_API2 CCompilerSet : public core::IReferenceCounted } protected: -#ifdef _NBL_PLATFORM_WINDOWS_ +#if defined(_NBL_PLATFORM_WINDOWS_) || defined(_NBL_PLATFORM_LINUX_) // DXC is available on both core::smart_refctd_ptr m_HLSLCompiler = nullptr; #endif core::smart_refctd_ptr m_GLSLCompiler = nullptr; diff --git a/include/nbl/asset/utils/CDirQuantCacheBase.h b/include/nbl/asset/utils/CDirQuantCacheBase.h index 1af32e2e2f..e4b421af0d 100644 --- a/include/nbl/asset/utils/CDirQuantCacheBase.h +++ b/include/nbl/asset/utils/CDirQuantCacheBase.h @@ -465,7 +465,7 @@ class CDirQuantCacheBase : public virtual core::IReferenceCounted, public impl:: // const float maxDirectionComp = value[maxDirCompIndex]; //max component of 3d normal cannot be less than sqrt(1/D) - if (maxDirectionComp < std::sqrtf(0.999683f / float(dimensions))) + if (maxDirectionComp < std::sqrt(0.999683f / float(dimensions))) { _NBL_DEBUG_BREAK_IF(true); return float32_tN(0.f); diff --git a/include/nbl/asset/utils/CGeometryCreator.h b/include/nbl/asset/utils/CGeometryCreator.h index 290c81b239..fa5ddba748 100644 --- a/include/nbl/asset/utils/CGeometryCreator.h +++ b/include/nbl/asset/utils/CGeometryCreator.h @@ -27,7 +27,9 @@ class NBL_API2 CGeometryCreator final : public core::IReferenceCounted core::smart_refctd_ptr normalCache = nullptr; core::smart_refctd_ptr quaternionCache = nullptr; }; - inline CGeometryCreator(SCreationParams&& params={}) : m_params(std::move(params)) + // separate overload because Clang can't evaluate `SCreationParams`'s default member initializers in a default argument here + inline CGeometryCreator() : CGeometryCreator(SCreationParams{}) {} + inline CGeometryCreator(SCreationParams&& params) : m_params(std::move(params)) { if (!m_params.normalCache) m_params.normalCache = core::make_smart_refctd_ptr(); diff --git a/include/nbl/asset/utils/CHLSLCompiler.h b/include/nbl/asset/utils/CHLSLCompiler.h index 7b942cae4f..8401089f0d 100644 --- a/include/nbl/asset/utils/CHLSLCompiler.h +++ b/include/nbl/asset/utils/CHLSLCompiler.h @@ -10,7 +10,7 @@ -#ifdef _NBL_PLATFORM_WINDOWS_ +#if defined(_NBL_PLATFORM_WINDOWS_) || defined(_NBL_PLATFORM_LINUX_) // DXC is available on both namespace nbl::asset::impl { diff --git a/include/nbl/asset/utils/CVertexHashGrid.h b/include/nbl/asset/utils/CVertexHashGrid.h index ce094fabe4..e7db2dcfed 100644 --- a/include/nbl/asset/utils/CVertexHashGrid.h +++ b/include/nbl/asset/utils/CVertexHashGrid.h @@ -182,7 +182,7 @@ class CVertexHashGrid const auto skipListBound = std::visit([&](auto& sorter) { auto hashBound = sorter.getMostSignificantRadixBound(hash); - return std::pair(m_vertices.begin() + hashBound.first, m_vertices.begin() + hashBound.second); + return std::pair(m_vertices.begin() + hashBound.first, m_vertices.begin() + hashBound.second); }, m_sorter); auto begin = std::lower_bound( diff --git a/include/nbl/asset/utils/IShaderCompiler.h b/include/nbl/asset/utils/IShaderCompiler.h index 432276ea32..a7560d914d 100644 --- a/include/nbl/asset/utils/IShaderCompiler.h +++ b/include/nbl/asset/utils/IShaderCompiler.h @@ -143,7 +143,9 @@ class NBL_API2 IShaderCompiler : public core::IReferenceCounted inline core::smart_refctd_ptr getDefaultFileSystemLoader() const { return m_defaultFileSystemLoader; } - void addSearchPath(const std::string& searchPath, const core::smart_refctd_ptr& loader, IncludeClassification classification = {}); + void addSearchPath(const std::string& searchPath, const core::smart_refctd_ptr& loader, IncludeClassification classification); + // separate overload because Clang can't evaluate `IncludeClassification`'s default member initializers in a default argument here + inline void addSearchPath(const std::string& searchPath, const core::smart_refctd_ptr& loader) {addSearchPath(searchPath,loader,IncludeClassification{});} void addGenerator(const core::smart_refctd_ptr& generator, IncludeClassification classification = {IncludeRootOrigin::Generated,HeaderClass::System}); @@ -291,7 +293,7 @@ class NBL_API2 IShaderCompiler : public core::IReferenceCounted requestingSourceDir(_requestingSourceDir), identifier(_identifier), standardInclude(_standardInclude), hash(_hash) {} - inline SPreprocessingDependency(SPreprocessingDependency&) = default; + inline SPreprocessingDependency(const SPreprocessingDependency&) = default; inline SPreprocessingDependency& operator=(SPreprocessingDependency&) = delete; inline SPreprocessingDependency(SPreprocessingDependency&&) = default; inline SPreprocessingDependency& operator=(SPreprocessingDependency&&) = default; diff --git a/include/nbl/builtin/hlsl/cpp_compat/impl/intrinsics_impl.hlsl b/include/nbl/builtin/hlsl/cpp_compat/impl/intrinsics_impl.hlsl index e2d21278dc..0143218be7 100644 --- a/include/nbl/builtin/hlsl/cpp_compat/impl/intrinsics_impl.hlsl +++ b/include/nbl/builtin/hlsl/cpp_compat/impl/intrinsics_impl.hlsl @@ -344,7 +344,8 @@ struct transpose_helper { using traits = matrix_traits; // GLM's transpose function signature specializes in terms of the input argument - return reinterpret_cast(glm::transpose(reinterpret_cast(m))); + const auto transposed = glm::transpose(reinterpret_cast(m)); + return reinterpret_cast(transposed); } }; template @@ -459,7 +460,8 @@ struct inverse_helper static SquareMatrix __call(NBL_CONST_REF_ARG(SquareMatrix) mat) { using traits = matrix_traits; - return reinterpret_cast(glm::inverse(reinterpret_cast(mat))); + const auto inv = glm::inverse(reinterpret_cast(mat)); + return reinterpret_cast(inv); } }; diff --git a/include/nbl/builtin/hlsl/cpp_compat/vector.hlsl b/include/nbl/builtin/hlsl/cpp_compat/vector.hlsl index 3512079eef..0fb7e15c52 100644 --- a/include/nbl/builtin/hlsl/cpp_compat/vector.hlsl +++ b/include/nbl/builtin/hlsl/cpp_compat/vector.hlsl @@ -8,8 +8,22 @@ #include #define GLM_FORCE_SWIZZLE +// GLM only provides swizzle members (`v.xyz`) when it detects MS language extensions (anonymous structs in unions), +// its only other route is the SIMD path which disables constexpr. GCC and Clang support the extension too, +// so make GLM's one-time configuration (glm/detail/setup.hpp) see it, exactly like it does with MSVC. +#if !defined(_MSC_EXTENSIONS) && (defined(__clang__) || defined(__GNUC__)) +#define _NBL_GLM_FAKE_MSC_EXTENSIONS_ +#define _MSC_EXTENSIONS 1 +#endif #include #include +#ifdef _NBL_GLM_FAKE_MSC_EXTENSIONS_ +#undef _MSC_EXTENSIONS +#undef _NBL_GLM_FAKE_MSC_EXTENSIONS_ +#if GLM_CONFIG_SWIZZLE != GLM_SWIZZLE_OPERATOR +#error "glm/glm.hpp was included before nbl/builtin/hlsl/cpp_compat/vector.hlsl, Nabla needs GLM swizzle operators" +#endif +#endif #include "nbl/core/hash/blake.h" #include "nbl/core/algorithm/utility.h" diff --git a/include/nbl/builtin/hlsl/format/shared_exp.hlsl b/include/nbl/builtin/hlsl/format/shared_exp.hlsl index ef09cd8a0f..d0ae36b0b5 100644 --- a/include/nbl/builtin/hlsl/format/shared_exp.hlsl +++ b/include/nbl/builtin/hlsl/format/shared_exp.hlsl @@ -161,7 +161,7 @@ struct static_cast_helper< decode_t v = val[i]; if (limits_t::is_signed) v = abs(v); - exponentsDecBias[i] = uint16_t(asuint(v)>>dec_MantissaStoredBits); + exponentsDecBias[i] = uint16_t(bit_cast(v)>>dec_MantissaStoredBits); } // get the maximum exponent diff --git a/include/nbl/builtin/hlsl/limits.hlsl b/include/nbl/builtin/hlsl/limits.hlsl index 6a0f5ddecd..0c53595a3a 100644 --- a/include/nbl/builtin/hlsl/limits.hlsl +++ b/include/nbl/builtin/hlsl/limits.hlsl @@ -12,7 +12,7 @@ // C++ headers #ifndef __HLSL_VERSION #include -#include "IMath/halfLimits.h" +#include "Imath/halfLimits.h" #endif /* diff --git a/include/nbl/core/alloc/SimpleBlockBasedAllocator.h b/include/nbl/core/alloc/SimpleBlockBasedAllocator.h index ae9ffb62ae..aa0d90c3a7 100644 --- a/include/nbl/core/alloc/SimpleBlockBasedAllocator.h +++ b/include/nbl/core/alloc/SimpleBlockBasedAllocator.h @@ -316,7 +316,7 @@ class SimpleBlockBasedAllocator final : protected using block_id_alloc_t = PoolAddressAllocatorST; public: - using handle_value_type = typename HandleValue; + using handle_value_type = HandleValue; using addr_alloc_traits = typename base_t::addr_alloc_traits; using extra_params_type = typename base_t::extra_params_type; using size_type = typename base_t::size_type; @@ -512,12 +512,12 @@ class SimpleBlockBasedAllocatorMT final template requires (!std::is_const_v) inline T* deref(typed_pointer_type p) { - return m_composed.deref(p); + return m_composed.template deref(p); } template requires std::is_const_v inline T* deref(typed_pointer_type p) const { - return m_composed.deref(p); + return m_composed.template deref(p); } // diff --git a/include/nbl/core/alloc/refctd_memory_resource.h b/include/nbl/core/alloc/refctd_memory_resource.h index 8d9eaf8aae..f613ecc33b 100644 --- a/include/nbl/core/alloc/refctd_memory_resource.h +++ b/include/nbl/core/alloc/refctd_memory_resource.h @@ -8,6 +8,9 @@ #include "BuildConfigOptions.h" #include "nbl/core/IReferenceCounted.h" +#include "nbl/core/decl/smart_refctd_ptr.h" + +#include namespace nbl::core diff --git a/include/nbl/core/containers/CMemoryPool.h b/include/nbl/core/containers/CMemoryPool.h index 04663c6134..3293f4e191 100644 --- a/include/nbl/core/containers/CMemoryPool.h +++ b/include/nbl/core/containers/CMemoryPool.h @@ -49,17 +49,17 @@ class CMemoryPool final : public Uncopyable template requires (!std::is_const_v) inline T* deref(typed_pointer_type p) { - return m_block_alctr.deref(p); + return m_block_alctr.template deref(p); } template inline const T* deref(typed_pointer_type p) const { - return m_block_alctr.deref(p); + return m_block_alctr.template deref(p); } template inline typed_pointer_type _dynamic_cast(const typed_pointer_type h) const { - return m_block_alctr._dynamic_cast(h); + return m_block_alctr.template _dynamic_cast(h); } // diff --git a/include/nbl/core/containers/CObjectPool.h b/include/nbl/core/containers/CObjectPool.h index 0044ea985e..0c7eb2b40e 100644 --- a/include/nbl/core/containers/CObjectPool.h +++ b/include/nbl/core/containers/CObjectPool.h @@ -104,7 +104,7 @@ class CObjectPool final : public IObjectPoolBase template requires (!std::is_const_v) inline T* deref(const typed_pointer_type h, check_t check={.value=false}) { - T* retval = m_pool.deref(h); + T* retval = m_pool.template deref(h); if (!retval) return nullptr; // check double free @@ -130,7 +130,7 @@ class CObjectPool final : public IObjectPoolBase template// requires (std::is_const_v == std::is_const_v) inline typed_pointer_type _dynamic_cast(const typed_pointer_type h) const { - return m_pool._dynamic_cast(h); + return m_pool.template _dynamic_cast(h); } // diff --git a/include/nbl/core/containers/DoublyLinkedList.h b/include/nbl/core/containers/DoublyLinkedList.h index d6757d9843..feafab407f 100644 --- a/include/nbl/core/containers/DoublyLinkedList.h +++ b/include/nbl/core/containers/DoublyLinkedList.h @@ -401,7 +401,7 @@ class DoublyLinkedList::Iterator { using base_iterable_t = DoublyLinkedList; using iterable_t = std::conditional_t; - friend class base_iterable_t; + friend base_iterable_t; public: using value_type = std::conditional_t; using pointer = value_type*; diff --git a/include/nbl/core/hash/blake.h b/include/nbl/core/hash/blake.h index a50d587e86..fb81d7fa81 100644 --- a/include/nbl/core/hash/blake.h +++ b/include/nbl/core/hash/blake.h @@ -10,6 +10,8 @@ #include #include +#include +#include namespace nbl::core @@ -38,8 +40,8 @@ class NBL_API2 blake3_hasher final { // unfortunately there's no concept like StandardLayout or Aggregate for "just structs/classes of non-pointer types" so need to play it safe constexpr bool ForbiddenType = std::is_compound_v || std::is_enum_v || std::is_class_v; - // use __FUNCTION__ to print something with `T` to the error log - static_assert(!ForbiddenType, __FUNCTION__ "Hashing Specialization for this Type is not implemented!"); + // the failing instantiation in the error log shows the offending `T` + static_assert(!ForbiddenType, "Hashing Specialization for this Type is not implemented!"); hasher.update(&input,sizeof(input)); } }; diff --git a/include/nbl/system/IApplicationFramework.h b/include/nbl/system/IApplicationFramework.h index 53af9c9b94..82bdf6bd5d 100644 --- a/include/nbl/system/IApplicationFramework.h +++ b/include/nbl/system/IApplicationFramework.h @@ -11,6 +11,8 @@ #include "nbl/system/CColoredStdoutLoggerWin32.h" #elif defined(_NBL_PLATFORM_ANDROID_) #include "nbl/system/CStdoutLoggerAndroid.h" +#elif defined(_NBL_PLATFORM_LINUX_) +#include #endif #include "nbl/system/CSystemAndroid.h" #include "nbl/system/CSystemLinux.h" @@ -67,9 +69,31 @@ class IApplicationFramework : public core::IReferenceCounted if (FAILED(hook)) return false; } - #else - // nothing else needs to be done cause we have RPath - // TODO: to be checked when time comes + #elif defined(_NBL_PLATFORM_LINUX_) + // modules linked at build time are already mapped by the dynamic linker through RUNPATH, + // otherwise fall back to the lookup paths (build tree or install layout) + std::string soName(moduleName); + if (soName.rfind("lib",0)!=0) + soName = "lib"+soName; + soName += ".so"; + + if (not dlopen(soName.c_str(),RTLD_LAZY|RTLD_NOLOAD)) + { + bool loaded = false; + for (const auto& dir : searchPaths) + { + if (dir.empty()) + continue; + const auto candidate = dir/soName; + if (std::filesystem::exists(candidate) && dlopen(candidate.string().c_str(),RTLD_NOW|RTLD_GLOBAL)) + { + loaded = true; + break; + } + } + if (not loaded) + return false; + } #endif return true; @@ -115,6 +139,8 @@ class IApplicationFramework : public core::IReferenceCounted return nbl::core::make_smart_refctd_ptr(); #elif defined(_NBL_PLATFORM_ANDROID_) return nullptr; + #elif defined(_NBL_PLATFORM_LINUX_) + return nbl::core::make_smart_refctd_ptr(); #endif return nullptr; } diff --git a/include/nbl/system/IFileBase.h b/include/nbl/system/IFileBase.h index c9ceb13a04..d97b036fa5 100644 --- a/include/nbl/system/IFileBase.h +++ b/include/nbl/system/IFileBase.h @@ -8,6 +8,7 @@ #include "nbl/system/path.h" +#include #include #include diff --git a/include/nbl/system/to_string.h b/include/nbl/system/to_string.h index d746f66ce6..4adbf3a9b5 100644 --- a/include/nbl/system/to_string.h +++ b/include/nbl/system/to_string.h @@ -118,7 +118,7 @@ struct to_string_helper> using value_t = hlsl::morton::code; static std::string __call(value_t value) { - return to_string_helper::__call(value.value); + return to_string_helper::__call(value.value); } }; diff --git a/include/nbl/video/IDeviceMemoryAllocation.h b/include/nbl/video/IDeviceMemoryAllocation.h index 019893040a..77c5c4a2ce 100644 --- a/include/nbl/video/IDeviceMemoryAllocation.h +++ b/include/nbl/video/IDeviceMemoryAllocation.h @@ -214,8 +214,10 @@ class NBL_API2 IDeviceMemoryAllocation : public virtual core::IReferenceCounted protected: + // separate overload because Clang can't evaluate `SCreationParams`'s default member initializers in a default argument here + inline IDeviceMemoryAllocation(const ILogicalDevice* originDevice) : IDeviceMemoryAllocation(originDevice,SCreationParams{}) {} IDeviceMemoryAllocation( - const ILogicalDevice* originDevice, SCreationParams&& params = {}) + const ILogicalDevice* originDevice, SCreationParams&& params) : m_originDevice(originDevice) , m_params(std::move(params)) , m_mappedPtr(nullptr) diff --git a/include/nbl/video/IGPUAccelerationStructure.h b/include/nbl/video/IGPUAccelerationStructure.h index 28b2695d52..1e59c42bcb 100644 --- a/include/nbl/video/IGPUAccelerationStructure.h +++ b/include/nbl/video/IGPUAccelerationStructure.h @@ -684,13 +684,13 @@ class IGPUTopLevelAccelerationStructure : public asset::ITopLevelAccelerationStr switch (instance.getType()) { case INSTANCE_TYPE::SRT_MOTION: - retval = convertInstance(std::get(instance.instance),gpuBLAS); + retval = convertInstance(std::get(instance.instance),gpuBLAS); break; case INSTANCE_TYPE::MATRIX_MOTION: - retval = convertInstance(std::get(instance.instance),gpuBLAS); + retval = convertInstance(std::get(instance.instance),gpuBLAS); break; default: - retval = convertInstance(std::get(instance.instance),gpuBLAS); + retval = convertInstance(std::get(instance.instance),gpuBLAS); break; } return retval; @@ -764,6 +764,16 @@ class IGPUTopLevelAccelerationStructure : public asset::ITopLevelAccelerationStr const uint32_t m_maxInstanceCount; private: + struct DynamicUpCastingSpanIterator + { + inline bool operator!=(const DynamicUpCastingSpanIterator& other) const {return ptr!=other.ptr;} + + inline DynamicUpCastingSpanIterator operator++() {return {++ptr};} + + inline blas_smart_ptr_t operator*() const {return core::smart_refctd_ptr_dynamic_cast(*ptr);} + + std::span>::iterator ptr; + }; friend class ILogicalDevice; friend class IQueue; diff --git a/include/nbl/video/IGPUCommandPool.h b/include/nbl/video/IGPUCommandPool.h index 22d8d0d3fe..df46fc3924 100644 --- a/include/nbl/video/IGPUCommandPool.h +++ b/include/nbl/video/IGPUCommandPool.h @@ -322,6 +322,7 @@ class IGPUCommandPool : public IBackendObject }; // I can't refactor this into a base class for tracking handles, cause I want them to live at the end :( class CCommandSegmentListPool; + public: // keep the access of the earlier declaration class IVariableSizeCommandBase : public ICommand { public: @@ -368,6 +369,7 @@ class IGPUCommandPool : public IBackendObject CExtraResourceTrackingBlock* m_next; }; + protected: template class IVariableSizeCommand : public IVariableSizeCommandBase { @@ -483,6 +485,7 @@ class IGPUCommandPool : public IBackendObject static_assert(sizeof(CCommandSegment)==CommandSegmentSize); private: + public: // keep the access of the earlier declaration class CExtraResourceTrackingBlock final : public IVariableSizeCommandBase { public: @@ -500,6 +503,8 @@ class IGPUCommandPool : public IBackendObject return extraResourceCount; } }; + private: + protected: // keep the access of the earlier declaration class CCommandSegmentListPool { public: @@ -636,6 +641,7 @@ class IGPUCommandPool : public IBackendObject }; core::CMemoryPool m_pool; }; + private: const core::bitflag m_flags; const uint8_t m_familyIx; diff --git a/include/nbl/video/ILogicalDevice.h b/include/nbl/video/ILogicalDevice.h index 5c88d55a6a..c73dd3a6a3 100644 --- a/include/nbl/video/ILogicalDevice.h +++ b/include/nbl/video/ILogicalDevice.h @@ -53,6 +53,8 @@ class NBL_API2 ILogicalDevice : public core::IReferenceCounted, public IDeviceMe inline system::ILogger* getLogger() const {return m_logger.get();} inline const IPhysicalDevice* getPhysicalDevice() const { return m_physicalDevice; } + // out of line because `IPhysicalDevice` is incomplete here and templates below need it + bool supportsAccelerationStructureVertexFormat(const asset::E_FORMAT format) const; inline const SPhysicalDeviceFeatures& getEnabledFeatures() const { return m_enabledFeatures; } @@ -451,7 +453,7 @@ class NBL_API2 ILogicalDevice : public core::IReferenceCounted, public IDeviceMe NBL_LOG_ERROR("Primitive type is Triangles but build flag says BLAS build is AABBs"); return {}; } - if (!getPhysicalDevice()->getBufferFormatUsages()[geom.vertexFormat].accelerationStructureVertex) + if (!supportsAccelerationStructureVertexFormat(geom.vertexFormat)) { NBL_LOG_ERROR("Vertex Format %d not supported as Acceleration Structure Vertex Position Input on this Device",geom.vertexFormat); return {}; @@ -580,7 +582,7 @@ class NBL_API2 ILogicalDevice : public core::IReferenceCounted, public IDeviceMe auto tlas = set.first; // we know the build is completed immediately after performing it, so we get our pending stamp then // ideally we should get our build version when the work of the deferred op gets executed for the first time - const auto buildVer = tlas->pushTrackedBLASes({set.second.begin()},{set.second.end()}); + const auto buildVer = tlas->pushTrackedBLASes({set.second.begin()},set.second.size()); tlas->clearTrackedBLASes(buildVer); } } @@ -681,8 +683,7 @@ class NBL_API2 ILogicalDevice : public core::IReferenceCounted, public IDeviceMe // we know the build is completed immediately after performing it, so we get our pending stamp then // ideally we should get the BLAS set from the Source TLAS when the work of the deferred op gets executed for the first time const auto* pSrcBLASes = src->getPendingBuildTrackedBLASes(src->getPendingBuildVer()); - const std::span emptySpan = {}; - buildVer = pSrcBLASes ? dst->pushTrackedBLASes(pSrcBLASes->begin(),pSrcBLASes->end()):dst->pushTrackedBLASes(emptySpan.begin(),emptySpan.end()); + buildVer = pSrcBLASes ? dst->pushTrackedBLASes(pSrcBLASes->begin(),pSrcBLASes->size()):dst->pushTrackedBLASes(nullptr,0); } dst->clearTrackedBLASes(buildVer); } @@ -788,7 +789,7 @@ class NBL_API2 ILogicalDevice : public core::IReferenceCounted, public IDeviceMe // upon completion set the BLASes tracked inline void operator()(IDeferredOperation*) const { - const auto buildVer = dst->pushTrackedBLASes({src->begin()},{src->end()}); + const auto buildVer = dst->pushTrackedBLASes({src.begin()},src.size()); dst->clearTrackedBLASes(buildVer); } diff --git a/include/nbl/video/IPhysicalDevice.h b/include/nbl/video/IPhysicalDevice.h index 229af7c116..cb518d3af9 100644 --- a/include/nbl/video/IPhysicalDevice.h +++ b/include/nbl/video/IPhysicalDevice.h @@ -48,7 +48,7 @@ class NBL_API2 IPhysicalDevice : public core::Interface, public core::Unmovable using cur = std::remove_cvref_t(key))>; if constexpr (is_bitflag) - core::hash_combine(seed, cur::UNDERLYING_TYPE(std::get(key).value)); + core::hash_combine(seed, typename cur::UNDERLYING_TYPE(std::get(key).value)); else if constexpr (std::is_convertible_v) core::hash_combine(seed, size_t(std::get(key))); else diff --git a/include/nbl/video/utilities/CAssetConverter.h b/include/nbl/video/utilities/CAssetConverter.h index 0419a4e10d..cf31630e54 100644 --- a/include/nbl/video/utilities/CAssetConverter.h +++ b/include/nbl/video/utilities/CAssetConverter.h @@ -492,11 +492,13 @@ class CAssetConverter : public core::IReferenceCounted private: // + // no default member initializers, Clang can't use them while `CHashCache` is incomplete (the hash maps below + // get instantiated during its definition) and silently drops every `HashEquals::operator()` taking a `key_t` template struct key_t { - core::smart_refctd_ptr asset = {}; - patch_t patch = {}; + core::smart_refctd_ptr asset; + patch_t patch; }; template struct HashEquals @@ -541,12 +543,12 @@ class CAssetConverter : public core::IReferenceCounted template inline container_t::iterator find(const lookup_t& assetAndPatch) { - return std::get>(m_containers).find>(assetAndPatch); + return std::get>(m_containers).template find>(assetAndPatch); } template inline container_t::const_iterator find(const lookup_t& assetAndPatch) const { - return std::get>(m_containers).find>(assetAndPatch); + return std::get>(m_containers).template find>(assetAndPatch); } template inline container_t::const_iterator end() const @@ -1138,9 +1140,13 @@ class CAssetConverter : public core::IReferenceCounted inline void setBuildFlags(const build_f _flags) {buildFlags = static_cast(_flags);} inline build_f getBuildFlags() const {return static_cast(buildFlags);} - core::smart_refctd_ptr canonical = nullptr; - uint64_t scratchSize : 47 = 0; - uint64_t buildFlags : 16 = 0; + // a constructor instead of default member initializers, Clang can't use those while `SReserveResult` is incomplete + // (its maps of this type get instantiated during its definition) and silently drops code that default constructs us + inline SConvReqAccelerationStructure() : canonical(nullptr), scratchSize(0), buildFlags(0), compact(0), buildSize(0) {} + + core::smart_refctd_ptr canonical; + uint64_t scratchSize : 47; + uint64_t buildFlags : 16; uint64_t compact : 1; // scratch + input size also accounting for worst case padding due to alignment uint64_t buildSize; diff --git a/include/nbl/video/utilities/CComputeBlit.h b/include/nbl/video/utilities/CComputeBlit.h index c191fb9cda..c446b1fe77 100644 --- a/include/nbl/video/utilities/CComputeBlit.h +++ b/include/nbl/video/utilities/CComputeBlit.h @@ -7,6 +7,7 @@ namespace nbl::video { +class CAssetConverter; class CComputeBlit : public core::IReferenceCounted { diff --git a/include/nbl/video/utilities/IUtilities.h b/include/nbl/video/utilities/IUtilities.h index 248dc9748f..6b3d1d7b0d 100644 --- a/include/nbl/video/utilities/IUtilities.h +++ b/include/nbl/video/utilities/IUtilities.h @@ -712,7 +712,7 @@ class NBL_API2 IUtilities : public core::IReferenceCounted { return downloadBufferRangeViaStagingBuffer(default_data_consumption_callback_t(data),nextSubmit,srcBufferRange); }; - if (autoSubmit(submit,lambda).copy()!=IQueue::RESULT::SUCCESS) + if (autoSubmit(submit,lambda).template copy()!=IQueue::RESULT::SUCCESS) return false; //! NOTE this method cannot be turned into a pure autoSubmitAndBlock + lambda because there's stuff to do AFTER the semaphore wait~! diff --git a/src/nbl/asset/ICPUPolygonGeometry.cpp b/src/nbl/asset/ICPUPolygonGeometry.cpp index 1af3c8c5d6..677dacf8bf 100644 --- a/src/nbl/asset/ICPUPolygonGeometry.cpp +++ b/src/nbl/asset/ICPUPolygonGeometry.cpp @@ -70,7 +70,7 @@ class CTriangleStripIndexingCB final : public IPolygonGeometryBase::IIndexingCal indexOfIndex = ctx.beginPrimitive+2; const int32_t perm[] = {-1,-2,0}; for (const auto end=ctx.endPrimitive+2; indexOfIndex!=end; indexOfIndex++) - ctx.streamOut>(indexOfIndex,perm); + ctx.template streamOut>(indexOfIndex,perm); } public: @@ -106,7 +106,7 @@ class CTriangleFanIndexingCB final : public IPolygonGeometryBase::IIndexingCallb { // first index is always global 0 perm[0] = -indexOfIndex; - ctx.streamOut>(indexOfIndex,perm); + ctx.template streamOut>(indexOfIndex,perm); } } diff --git a/src/nbl/asset/material_compiler3/CFrontendIR.cpp b/src/nbl/asset/material_compiler3/CFrontendIR.cpp index 9ea0a59876..f5cbb68fa5 100644 --- a/src/nbl/asset/material_compiler3/CFrontendIR.cpp +++ b/src/nbl/asset/material_compiler3/CFrontendIR.cpp @@ -1398,5 +1398,5 @@ auto CFrontendIR::CCookTorrance::createIRNode(const bool forBTDF, const CFronten return retval; } -template class CTrueIR::CSpectralVariable; +// explicitly instantiated in the header already, a second one in the same TU is ill-formed (Clang rejects it) } \ No newline at end of file diff --git a/src/nbl/asset/material_compiler3/CTrueIR.cpp b/src/nbl/asset/material_compiler3/CTrueIR.cpp index cfcf1f3a1b..4864f3e099 100644 --- a/src/nbl/asset/material_compiler3/CTrueIR.cpp +++ b/src/nbl/asset/material_compiler3/CTrueIR.cpp @@ -325,5 +325,5 @@ void CTrueIR::CEmitter::printDot(std::ostringstream& sstr, const core::string& s } } -template class CTrueIR::CSpectralVariable; +// explicitly instantiated in the header already, a second one in the same TU is ill-formed (Clang rejects it) } \ No newline at end of file diff --git a/src/nbl/asset/utils/CCompilerSet.cpp b/src/nbl/asset/utils/CCompilerSet.cpp index e1b161f4e3..14c15c7668 100644 --- a/src/nbl/asset/utils/CCompilerSet.cpp +++ b/src/nbl/asset/utils/CCompilerSet.cpp @@ -15,7 +15,7 @@ core::smart_refctd_ptr CCompilerSet::compileToSPIRV(const IShader* shad { case IShader::E_CONTENT_TYPE::ECT_HLSL: { -#ifdef _NBL_PLATFORM_WINDOWS_ +#if defined(_NBL_PLATFORM_WINDOWS_) || defined(_NBL_PLATFORM_LINUX_) // DXC is available on both const char* code = reinterpret_cast(shader->getContent()->getPointer()); outSpirvShader = m_HLSLCompiler->compileToSPIRV(code, options); #endif @@ -45,7 +45,7 @@ core::smart_refctd_ptr CCompilerSet::preprocessShader(const IShader* sh { case IShader::E_CONTENT_TYPE::ECT_HLSL: { -#ifdef _NBL_PLATFORM_WINDOWS_ +#if defined(_NBL_PLATFORM_WINDOWS_) || defined(_NBL_PLATFORM_LINUX_) // DXC is available on both const char* code = reinterpret_cast(shader->getContent()->getPointer()); auto resolvedCode = m_HLSLCompiler->preprocessShader(code, stage, preprocessOptions); return core::make_smart_refctd_ptr(resolvedCode.c_str(), IShader::E_CONTENT_TYPE::ECT_HLSL, std::string(shader->getFilepathHint())); diff --git a/src/nbl/asset/utils/CHLSLCompiler.cpp b/src/nbl/asset/utils/CHLSLCompiler.cpp index c378048180..b249381e61 100644 --- a/src/nbl/asset/utils/CHLSLCompiler.cpp +++ b/src/nbl/asset/utils/CHLSLCompiler.cpp @@ -8,7 +8,7 @@ #include "spirv/builtin/CArchive.h" #endif // NBL_EMBED_BUILTIN_RESOURCES -#ifdef _NBL_PLATFORM_WINDOWS_ +#if defined(_NBL_PLATFORM_WINDOWS_) || defined(_NBL_PLATFORM_LINUX_) // DXC is available on both @@ -16,14 +16,28 @@ #include #include #include +#include +#ifdef _NBL_PLATFORM_WINDOWS_ #include #include -#include #include +using Microsoft::WRL::ComPtr; +#else +// DXC's WinAdapter provides the COM types on other platforms, give its `CComPtr` the bits of WRL's `ComPtr` used here +#include +#include +template +struct ComPtr : CComPtr +{ + using CComPtr::CComPtr; + using CComPtr::operator=; + inline T* Get() const {return this->p;} + inline T** GetAddressOf() {return &this->p;} +}; +#endif using namespace nbl; using namespace nbl::asset; -using Microsoft::WRL::ComPtr; static constexpr const wchar_t* SHADER_MODEL_PROFILE = L"XX_6_8"; static const wchar_t* ShaderStageToString(asset::IShader::E_SHADER_STAGE stage) { @@ -69,9 +83,9 @@ struct DXC struct DxcCompilationResult { - Microsoft::WRL::ComPtr errorMessages; - Microsoft::WRL::ComPtr objectBlob; - Microsoft::WRL::ComPtr compileResult; + ComPtr errorMessages; + ComPtr objectBlob; + ComPtr compileResult; std::string GetErrorMessagesString() { diff --git a/src/nbl/core/alloc/refctd_memory_resource.cpp b/src/nbl/core/alloc/refctd_memory_resource.cpp index 9eb9a991b4..9c8fb8b65f 100644 --- a/src/nbl/core/alloc/refctd_memory_resource.cpp +++ b/src/nbl/core/alloc/refctd_memory_resource.cpp @@ -4,6 +4,7 @@ #include "nbl/core/alloc/refctd_memory_resource.h" +#include "nbl/core/def/smart_refctd_ptr.h" #include diff --git a/src/nbl/system/CSystemLinux.cpp b/src/nbl/system/CSystemLinux.cpp index 58aaeeb51b..d28f5c03cb 100644 --- a/src/nbl/system/CSystemLinux.cpp +++ b/src/nbl/system/CSystemLinux.cpp @@ -6,9 +6,6 @@ using namespace nbl::system; #ifdef _NBL_PLATFORM_LINUX_ #include -#include -#include -#include ISystem::SystemInfo CSystemLinux::getSystemInfo() const { SystemInfo info; @@ -30,36 +27,4 @@ ISystem::SystemInfo CSystemLinux::getSystemInfo() const return info; } -bool isDebuggerAttached() -{ - constexpr char tracerPidStr[] = "TracerPid:"; - char buf[4096]; - - const int status = open("/proc/self/status"); - if (status == -1) - return false; - - const size_t numRead = read(status, static_cast(buf), sizeof(buf) - 1); - close(status); - - buf[numRead] = '\0'; - const auto offset = strstr(buf, tracerPidStr); - if (not offset) - return false; - - // few helper lambdas - auto isSpace = [](char c) { return c == ' '; }; - auto isDigit = [](char c) { return c >= '0' && c <= '9'; }; - - for (const char* cPtr = offset + sizeof(tracerPidStr) - 1; cPtr <= buf + numRead; cPtr++) - { - if (isSpace(*cPtr)) - continue; - else - return isDigit(*cPtr) && *cPtr != '0'; - } - - return false; -} - #endif \ No newline at end of file diff --git a/src/nbl/system/ISystem.cpp b/src/nbl/system/ISystem.cpp index d9c939be1a..35fe8fb718 100644 --- a/src/nbl/system/ISystem.cpp +++ b/src/nbl/system/ISystem.cpp @@ -390,8 +390,11 @@ bool ISystem::isDebuggerAttached() return false; char buf[bufSize]; - const size_t numRead = read(status, static_cast(buf), bufSize - 1); + const ssize_t bytesRead = read(status, static_cast(buf), bufSize - 1); close(status); + if (bytesRead <= 0) + return false; + const size_t numRead = static_cast(bytesRead); buf[numRead] = '\0'; const auto offset = strstr(buf, debuggerPidStr); @@ -402,7 +405,7 @@ bool ISystem::isDebuggerAttached() auto isSpace = [](const char c) { return c == ' ' || c == '\t'; }; auto isDigit = [](const char c) { return c >= '0' && c <= '9'; }; - for (const char* cPtr = offset + sizeof(debuggerPidStr) - 1; cPtr <= buf + numRead; cPtr++) + for (const char* cPtr = offset + sizeof(debuggerPidStr) - 1; cPtr < buf + numRead; cPtr++) { if (isSpace(*cPtr)) continue; diff --git a/src/nbl/video/CCUDAHandler.cpp b/src/nbl/video/CCUDAHandler.cpp index 738a493a4c..c325f49a7a 100644 --- a/src/nbl/video/CCUDAHandler.cpp +++ b/src/nbl/video/CCUDAHandler.cpp @@ -3,6 +3,7 @@ // For conditions of distribution and use, see copyright notice in nabla.h #include "nbl/video/CUDAInterop.h" +#include "nbl/system/ModuleLookupUtils.h" #include "nlohmann/json.hpp" diff --git a/src/nbl/video/CVulkanAccelerationStructure.h b/src/nbl/video/CVulkanAccelerationStructure.h index 4c0d67eee1..32e24903cd 100644 --- a/src/nbl/video/CVulkanAccelerationStructure.h +++ b/src/nbl/video/CVulkanAccelerationStructure.h @@ -104,7 +104,7 @@ void getVkASGeometryFrom(const IGPUBottomLevelAccelerationStructure::Triangles::HostTransform) outBase.geometry.triangles.transformData.hostAddress = &triangles.transform; else outBase.geometry.triangles.transformData = getVkDeviceOrHostAddress(triangles.transform); diff --git a/src/nbl/video/CVulkanMemoryAllocation.cpp b/src/nbl/video/CVulkanMemoryAllocation.cpp index a9b6341a82..9b9add6228 100644 --- a/src/nbl/video/CVulkanMemoryAllocation.cpp +++ b/src/nbl/video/CVulkanMemoryAllocation.cpp @@ -14,10 +14,10 @@ system::external_handle_t CVulkanMemoryAllocation::getExportHandle(E_EXTERNAL_HA { using U = typename core::bitflag::UNDERLYING_TYPE; - if (!std::has_single_bit(static_cast(handleType))) return nullptr; + if (!std::has_single_bit(static_cast(handleType))) return system::ExternalHandleNull; const auto externalHandleTypes = getCreationParams().externalHandleTypes; - if (!externalHandleTypes.hasFlags(handleType)) return nullptr; + if (!externalHandleTypes.hasFlags(handleType)) return system::ExternalHandleNull; const auto mask = core::bitflag(handleType - 1); const auto handleIndex = hlsl::bitCount(externalHandleTypes & mask); diff --git a/src/nbl/video/CVulkanSemaphore.cpp b/src/nbl/video/CVulkanSemaphore.cpp index 1e3ead69fd..60e009d953 100644 --- a/src/nbl/video/CVulkanSemaphore.cpp +++ b/src/nbl/video/CVulkanSemaphore.cpp @@ -47,9 +47,9 @@ system::external_handle_t CVulkanSemaphore::getExportHandle(E_EXTERNAL_HANDLE_TY { using U = typename core::bitflag::UNDERLYING_TYPE; - if (!std::has_single_bit(static_cast(handleType))) return nullptr; + if (!std::has_single_bit(static_cast(handleType))) return system::ExternalHandleNull; - if (!m_creationParams.externalHandleTypes.hasFlags(handleType)) return nullptr; + if (!m_creationParams.externalHandleTypes.hasFlags(handleType)) return system::ExternalHandleNull; const auto externalHandleTypes = m_creationParams.externalHandleTypes; const auto mask = core::bitflag(handleType - 1); diff --git a/src/nbl/video/IAPIConnection.cpp b/src/nbl/video/IAPIConnection.cpp index 05ababf2f1..aaa6a3c1f3 100644 --- a/src/nbl/video/IAPIConnection.cpp +++ b/src/nbl/video/IAPIConnection.cpp @@ -129,10 +129,10 @@ renderdoc_api_t* IAPIConnection::loadRenderdoc() RENDERDOC_GetAPI = (pRENDERDOC_GetAPI)GetProcAddress(mod,"RENDERDOC_GetAPI"); #elif defined(_NBL_PLATFORM_ANDROID_) if (void* mod=dlopen("libVkLayer_GLES_RenderDoc.so",RTLD_NOW|RTLD_NOLOAD); mod) - pRENDERDOC_GetAPI RENDERDOC_GetAPI = (pRENDERDOC_GetAPI)dlsym(mod,"RENDERDOC_GetAPI"); + RENDERDOC_GetAPI = (pRENDERDOC_GetAPI)dlsym(mod,"RENDERDOC_GetAPI"); #elif defined(_NBL_PLATFORM_LINUX_) if (void* mod=dlopen("librenderdoc.so",RTLD_NOW|RTLD_NOLOAD); mod) - pRENDERDOC_GetAPI RENDERDOC_GetAPI = (pRENDERDOC_GetAPI)dlsym(mod,"RENDERDOC_GetAPI"); + RENDERDOC_GetAPI = (pRENDERDOC_GetAPI)dlsym(mod,"RENDERDOC_GetAPI"); #else #error "Nabla Unsupported Platform!" #endif diff --git a/src/nbl/video/ILogicalDevice.cpp b/src/nbl/video/ILogicalDevice.cpp index 8a29e93d78..e515d3765d 100644 --- a/src/nbl/video/ILogicalDevice.cpp +++ b/src/nbl/video/ILogicalDevice.cpp @@ -169,6 +169,11 @@ const SPhysicalDeviceLimits& ILogicalDevice::getPhysicalDeviceLimits() const return m_physicalDevice->getLimits(); } +bool ILogicalDevice::supportsAccelerationStructureVertexFormat(const asset::E_FORMAT format) const +{ + return m_physicalDevice->getBufferFormatUsages()[format].accelerationStructureVertex; +} + bool ILogicalDevice::supportsMask(const uint32_t queueFamilyIndex, core::bitflag stageMask) const { if (getQueueCount(queueFamilyIndex) == 0) @@ -1840,7 +1845,7 @@ bool ILogicalDevice::createComputePipelines(IGPUPipelineCache* const pipelineCac { const auto& ci = params[ix]; - const core::set entryPoints = { asset::ISPIRVEntryPointTrimmer::EntryPoint{.name = ci.shader.entryPoint, .stage = hlsl::ShaderStage::ESS_COMPUTE} }; + const core::set entryPoints = { asset::ISPIRVEntryPointTrimmer::EntryPoint{.name = ci.shader.entryPoint, .stage = hlsl::ShaderStage::ESS_COMPUTE} }; trimmedShaders.push_back(m_spirvTrimmer->trim(ci.shader.shader, entryPoints, m_logger)); auto trimmedShaderSpec = ci.shader; trimmedShaderSpec.shader = trimmedShaders.back().get(); diff --git a/src/nbl/video/utilities/CAssetConverter.cpp b/src/nbl/video/utilities/CAssetConverter.cpp index 4f3d13ff18..f535bf8aeb 100644 --- a/src/nbl/video/utilities/CAssetConverter.cpp +++ b/src/nbl/video/utilities/CAssetConverter.cpp @@ -2456,7 +2456,7 @@ class MetaDeviceMemoryAllocator final const auto* memBacked = getAsBase(binItems[i]); const auto& memReqs = memBacked->getMemoryReqs(); // round up the offset to get the correct alignment - offsetsTmp[i] = core::roundUp(offsetsTmp[i],0x1ull<(offsetsTmp[i],0x1ull< SReserveResult .device = device, .dfsCaches = dfsCaches, .stack = stack - }.descend_impl_impl({},{asset,uniqueGroupID},std::move(patch)); + }.template descend_impl_impl({},{asset,uniqueGroupID},std::move(patch)); } }; core::for_each_in_tuple(inputs.assets,initialize); From c0598962086d40885d9a2c6c66be0ff9b2eff92d Mon Sep 17 00:00:00 2001 From: Raydelto Hernandez Date: Wed, 7 Oct 2026 20:14:24 -0300 Subject: [PATCH 3/9] NAB-7: Headless examples build and pass on Linux (engine side) (#3) * NAB-7: Keep POSIX absolute paths in shader include lookup CIncludeFinder stripped the leading '/' from search paths and from quoted include names, so nsc -I/-isystem roots and the absolute includes in generated .spv.config files became relative and were never found. ISystem::findFileInArchive called canonical("") for a relative existing file such as 'a.hlsl', which throws in libstdc++. Co-Authored-By: Claude Opus 5.5 Co-authored-by: multica-agent * NAB-7: Open POSIX files like Win32 OPEN_ALWAYS creat() forced O_WRONLY|O_TRUNC and mode 0444, so a writable file could not be read back and the next run could not reopen its own output (01_HelloCoreSystemAsset segfaulted on its second run in CFileLogger). Co-Authored-By: Claude Opus 5.5 Co-authored-by: multica-agent * NAB-7: Clang fixes for headers used by the examples - ICamera: '= 0' pure specifiers instead of '= 0u'. - isNearlyZeroVector and promoted_mul: hlsl::vector/matrix dimensions can't be deduced through a mismatched non-type parameter type in C++. - floatutil.h: include for memcpy. - spirv subgroup builtins are HLSL-only (C++ rejects const without init). - ui/declarations.h: the IWindowManager interface on Linux. Co-Authored-By: Claude Opus 5.5 Co-authored-by: multica-agent * NAB-7: Build ELF executables -fPIC so they can reuse library PCHs Clang refuses a PCH built -fPIC (static examples API library in a shared build) in an -fPIE translation unit. Co-Authored-By: Claude Opus 5.5 Co-authored-by: multica-agent * NAB-7: Use non-throwing filesystem::exists for absolute include names An unreadable leading-/ include path would otherwise throw and abort nsc. Co-Authored-By: Claude Opus 5.5 Co-authored-by: multica-agent --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: multica-agent --- cmake/common.cmake | 7 +++++++ .../nbl/builtin/hlsl/math/linalg/fast_affine.hlsl | 12 ++++++++++-- .../hlsl/spirv_intrinsics/subgroup_basic.hlsl | 2 ++ include/nbl/core/math/floatutil.h | 1 + include/nbl/ext/Cameras/CCameraMathUtilities.hpp | 5 +++-- include/nbl/ext/Cameras/ICamera.hpp | 6 +++--- include/nbl/ui/declarations.h | 1 + src/nbl/asset/utils/IShaderCompiler.cpp | 13 +++++++++++-- src/nbl/system/ISystem.cpp | 3 ++- src/nbl/system/ISystemPOSIX.cpp | 4 +++- 10 files changed, 43 insertions(+), 11 deletions(-) diff --git a/cmake/common.cmake b/cmake/common.cmake index ebafb7d2bc..edd0195f5e 100755 --- a/cmake/common.cmake +++ b/cmake/common.cmake @@ -79,6 +79,13 @@ macro(nbl_create_executable_project _EXTRA_SOURCES _EXTRA_OPTIONS _EXTRA_INCLUDE nbl_adjust_flags(TARGET ${EXECUTABLE_NAME} MAP_RELEASE Release MAP_RELWITHDEBINFO RelWithDebInfo MAP_DEBUG Debug) nbl_adjust_definitions() + # executables reuse PCHs of static libraries (e.g. the examples API), which are built -fPIC in a shared + # build, and Clang rejects a -fPIC PCH in an -fPIE translation unit; after nbl_adjust_flags, it resets the options + if(NOT WIN32 AND NOT ANDROID AND CMAKE_POSITION_INDEPENDENT_CODE) + set_target_properties(${EXECUTABLE_NAME} PROPERTIES POSITION_INDEPENDENT_CODE OFF) + target_compile_options(${EXECUTABLE_NAME} PRIVATE -fPIC) + endif() + add_compile_options(${_EXTRA_OPTIONS}) add_definitions(-D_NBL_PCH_IGNORE_PRIVATE_HEADERS) # TODO: wipe when we finally make Nabla PCH work as its supposed to set_target_properties(${EXECUTABLE_NAME} PROPERTIES diff --git a/include/nbl/builtin/hlsl/math/linalg/fast_affine.hlsl b/include/nbl/builtin/hlsl/math/linalg/fast_affine.hlsl index 1559a29d8b..cbb51af028 100644 --- a/include/nbl/builtin/hlsl/math/linalg/fast_affine.hlsl +++ b/include/nbl/builtin/hlsl/math/linalg/fast_affine.hlsl @@ -20,8 +20,15 @@ namespace math namespace linalg { +// `hlsl::matrix` has `uint16_t` dimensions in C++ and Clang won't deduce them through an `int` parameter, DXC wants `int` +#ifdef __HLSL_VERSION +#define NBL_PROMOTED_MUL_DIM_T int +#else +#define NBL_PROMOTED_MUL_DIM_T uint16_t +#endif + // Multiply matrices as-if extended to be filled with identity elements -template +template matrix promoted_mul(NBL_CONST_REF_ARG(matrix) lhs, NBL_CONST_REF_ARG(matrix) rhs) { matrix retval; @@ -56,7 +63,7 @@ matrix promoted_mul(NBL_CONST_REF_ARG(matrix) lhs, NBL_CONST_REF_A } // Multiply matrix and vector as-if extended to be filled with 1 in diagonal for matrix and last for vector -template +template vector promoted_mul(NBL_CONST_REF_ARG(matrix) lhs, const vector v) { vector retval; @@ -77,6 +84,7 @@ vector promoted_mul(NBL_CONST_REF_ARG(matrix) lhs, const vector } return retval; } +#undef NBL_PROMOTED_MUL_DIM_T template inline void setRotation(NBL_REF_ARG(matrix) outMat, NBL_CONST_REF_ARG(math::quaternion) quat) diff --git a/include/nbl/builtin/hlsl/spirv_intrinsics/subgroup_basic.hlsl b/include/nbl/builtin/hlsl/spirv_intrinsics/subgroup_basic.hlsl index 08d493b87a..02df40c2b3 100644 --- a/include/nbl/builtin/hlsl/spirv_intrinsics/subgroup_basic.hlsl +++ b/include/nbl/builtin/hlsl/spirv_intrinsics/subgroup_basic.hlsl @@ -14,6 +14,7 @@ namespace hlsl { namespace spirv { +#ifdef __HLSL_VERSION [[vk::ext_builtin_input(spv::BuiltInSubgroupSize)]] static const uint32_t SubgroupSize; [[vk::ext_builtin_input(spv::BuiltInNumSubgroups)]] @@ -25,6 +26,7 @@ static const uint32_t SubgroupLocalInvocationId; [[vk::ext_instruction( spv::OpGroupNonUniformElect )]] bool subgroupElect(uint32_t executionScope); +#endif } } } diff --git a/include/nbl/core/math/floatutil.h b/include/nbl/core/math/floatutil.h index d66e3d6275..d73eec487c 100644 --- a/include/nbl/core/math/floatutil.h +++ b/include/nbl/core/math/floatutil.h @@ -7,6 +7,7 @@ #include #include +#include #include #include diff --git a/include/nbl/ext/Cameras/CCameraMathUtilities.hpp b/include/nbl/ext/Cameras/CCameraMathUtilities.hpp index 2a992b27a4..84473fd461 100644 --- a/include/nbl/ext/Cameras/CCameraMathUtilities.hpp +++ b/include/nbl/ext/Cameras/CCameraMathUtilities.hpp @@ -84,8 +84,9 @@ struct CCameraMathUtilities final return hlsl::approx::absRelEqual(value, T(0), epsilon, T(0)); } - template - static inline bool isNearlyZeroVector(const hlsl::vector& value, const T epsilon) + // `hlsl::vector` is an alias of `glm::vec`, so a `uint32_t N` parameter can't be deduced through it + template + static inline bool isNearlyZeroVector(const Vec& value, const T epsilon) { return hlsl::approx::absRelEqual(hlsl::length(value), T(0), epsilon, T(0)); } diff --git a/include/nbl/ext/Cameras/ICamera.hpp b/include/nbl/ext/Cameras/ICamera.hpp index b34bc5c0b0..9e8824cf21 100644 --- a/include/nbl/ext/Cameras/ICamera.hpp +++ b/include/nbl/ext/Cameras/ICamera.hpp @@ -154,7 +154,7 @@ class ICamera : virtual public core::IReferenceCounted virtual ~ICamera() = default; /// @brief Return the gimbal holding the runtime camera pose. - virtual const CCameraGimbal& getGimbal() = 0u; + virtual const CCameraGimbal& getGimbal() = 0; /// @brief Apply one frame of physical deltas on top of the pose currently held by the gimbal. /// @@ -195,7 +195,7 @@ class ICamera : virtual public core::IReferenceCounted } /// @brief Return the `ECameraControlAxis` mask this rig applies. Every other axis must be zero in a frame passed to `manipulate(...)`. - virtual uint32_t getAcceptedControls() const = 0u; + virtual uint32_t getAcceptedControls() const = 0; /// @brief Return the stable camera-family identifier for this concrete runtime camera. virtual CameraKind getKind() const = 0; @@ -213,7 +213,7 @@ class ICamera : virtual public core::IReferenceCounted } /// @brief Return the stable human-readable identifier for this concrete camera instance. - virtual std::string_view getIdentifier() const = 0u; + virtual std::string_view getIdentifier() const = 0; /// @brief Check whether the camera exposes the requested optional capability. inline bool hasCapability(CameraCapability capability) const diff --git a/include/nbl/ui/declarations.h b/include/nbl/ui/declarations.h index 4a2bce9552..7b4936d998 100644 --- a/include/nbl/ui/declarations.h +++ b/include/nbl/ui/declarations.h @@ -13,6 +13,7 @@ #elif defined(_NBL_BUILD_WITH_WAYLAND) && defined(_NBL_TEST_WAYLAND) # include "nbl/ui/CWindowManagerWayland.h" #elif defined(_NBL_PLATFORM_LINUX_) +# include "nbl/ui/IWindowManager.h" // no native window manager yet, only the interface #endif // TODO more platforms (android) // clipboards diff --git a/src/nbl/asset/utils/IShaderCompiler.cpp b/src/nbl/asset/utils/IShaderCompiler.cpp index 82a3013afd..813b38a934 100644 --- a/src/nbl/asset/utils/IShaderCompiler.cpp +++ b/src/nbl/asset/utils/IShaderCompiler.cpp @@ -702,6 +702,10 @@ std::string normalizeIncludeLookupName(const std::string& includeName) second != '/' && second != '\\'; if (!hasSingleLeadingSeparator) return includeName; + // on POSIX a single leading separator is also an absolute filesystem path (`#include "/abs/file.hlsl"`) + std::error_code ec; + if (std::filesystem::path(includeName).is_absolute() && std::filesystem::exists(includeName,ec)) + return includeName; return includeName.substr(1ull); } @@ -917,10 +921,15 @@ void IShaderCompiler::CIncludeFinder::addSearchPath(const std::string& searchPat { if (!loader) return; - auto normalizedSearchPath = normalizeClassifiedRootPath(searchPath); + const auto normalizedSearchPath = normalizeClassifiedRootPath(searchPath); if (!normalizedSearchPath.empty()) registerHeaderRoot(normalizedSearchPath, classification); - m_loaders.emplace_back(LoaderSearchPath{ loader, std::move(normalizedSearchPath), classification }); + // the classified root drops a leading separator, the loader must keep it or POSIX absolute paths become relative + auto loaderSearchPath = searchPath; + std::replace(loaderSearchPath.begin(), loaderSearchPath.end(), '\\', '/'); + while (loaderSearchPath.size() > 1ull && loaderSearchPath.back() == '/') + loaderSearchPath.pop_back(); + m_loaders.emplace_back(LoaderSearchPath{ loader, std::move(loaderSearchPath), classification }); } void IShaderCompiler::CIncludeFinder::addGenerator(const core::smart_refctd_ptr& generatorToAdd, IncludeClassification classification) diff --git a/src/nbl/system/ISystem.cpp b/src/nbl/system/ISystem.cpp index 35fe8fb718..f8c78ca120 100644 --- a/src/nbl/system/ISystem.cpp +++ b/src/nbl/system/ISystem.cpp @@ -257,7 +257,8 @@ core::smart_refctd_ptr ISystem::openFileArchive(core::smart_refctd ISystem::FoundArchiveFile ISystem::findFileInArchive(const system::path& absolutePath) const { - system::path path = std::filesystem::exists(absolutePath) ? std::filesystem::canonical(absolutePath.parent_path()):absolutePath.parent_path(); + // a relative path to an existing file (e.g. `a.hlsl`) has an empty parent, and canonical("") throws in libstdc++ + system::path path = std::filesystem::exists(absolutePath) ? std::filesystem::canonical(std::filesystem::absolute(absolutePath).parent_path()):absolutePath.parent_path(); // going up the directory tree while (!path.empty() && path.parent_path()!=path) { diff --git a/src/nbl/system/ISystemPOSIX.cpp b/src/nbl/system/ISystemPOSIX.cpp index 6692a1e8b9..33f278b840 100644 --- a/src/nbl/system/ISystemPOSIX.cpp +++ b/src/nbl/system/ISystemPOSIX.cpp @@ -38,7 +38,9 @@ core::smart_refctd_ptr ISystemPOSIX::CCaller::createFile(const std: // only create a new file if we're going to be writing if (writeAccess) { - _native = creat(name_c_str, S_IRUSR | S_IRGRP | S_IROTH);//open(name_c_str, createFlags, S_IRUSR | S_IRGRP | S_IROTH); + // like `OPEN_ALWAYS` on Win32: keep the requested access mode, don't truncate, and leave the new file writable + // (`creat` meant `O_WRONLY|O_TRUNC` with mode 0444, so any second run failed to reopen its own output) + _native = open(name_c_str, createFlags, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); } else if (std::filesystem::exists(filename)) { From 4dcca0da0db6828206fe4aa96a33033bc3cb7f68 Mon Sep 17 00:00:00 2001 From: Raydelto Hernandez Date: Thu, 8 Oct 2026 00:33:08 -0300 Subject: [PATCH 4/9] NAB-7: Point examples_tests at the raydelto fork with the Linux fixes (#4) examples_tests now comes from raydelto/Nabla-Examples-and-Tests and is bumped to c337709e (raydelto/Nabla-Examples-and-Tests#1): the headless examples build on Linux, and test 23 caps its workgroup size by maxComputeWorkgroupSubgroups so it no longer hangs Intel GPUs. Co-authored-by: Claude Opus 5.5 Co-authored-by: multica-agent --- .gitmodules | 2 +- examples_tests | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index f96f66ad0d..008766eec9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -64,7 +64,7 @@ branch = main [submodule "examples_tests"] path = examples_tests - url = git@github.com:Devsh-Graphics-Programming/Nabla-Examples-and-Tests.git + url = git@github.com:raydelto/Nabla-Examples-and-Tests.git [submodule "3rdparty/dxc/dxc"] path = 3rdparty/dxc/dxc url = git@github.com:Devsh-Graphics-Programming/DirectXShaderCompiler.git diff --git a/examples_tests b/examples_tests index 4a0cc2f683..c337709e48 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 4a0cc2f68349ac188c5a9208ae1cadb2684b4040 +Subproject commit c337709e48b235225483f8877508c67ef140713a From f6666e500e6ad0bf6c98cbdf97a51f8224ac54b4 Mon Sep 17 00:00:00 2001 From: Raydelto Hernandez Date: Thu, 8 Oct 2026 00:52:13 -0300 Subject: [PATCH 5/9] NAB-8: Linux build guide (#5) * NAB-8: Linux build guide Co-authored-by: multica-agent * NAB-8: Document fresh-fork hydration recipe, use subshells for test runs, clarify pending Mesa report Co-authored-by: multica-agent --------- Co-authored-by: multica-agent --- docs/build/LINUX.md | 2 + docs/linux-build.md | 200 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 docs/linux-build.md diff --git a/docs/build/LINUX.md b/docs/build/LINUX.md index 9c189d60e9..dc0ee36aed 100644 --- a/docs/build/LINUX.md +++ b/docs/build/LINUX.md @@ -1,5 +1,7 @@ # Linux build +> For the up-to-date Linux build guide, toolchain setup, and validation steps, see [docs/linux-build.md](../linux-build.md). + ## Supported toolsets - **[GCC](https://gcc.gnu.org/)** diff --git a/docs/linux-build.md b/docs/linux-build.md new file mode 100644 index 0000000000..70ec51c238 --- /dev/null +++ b/docs/linux-build.md @@ -0,0 +1,200 @@ +# Building Nabla on Linux + +This guide details the prerequisites, toolchain setup, configuration, compilation, and testing procedures for Nabla on Linux (tested on Ubuntu 24.04 LTS). + +--- + +## 1. Prerequisites & Toolchain Setup + +### System Packages +Install the required build tools, XCB/X11 development libraries, and utilities via your package manager: + +```bash +sudo apt update +sudo apt install -y ninja-build lld nasm python3 git git-lfs ccache \ + libxcb-xkb-dev libxcb-randr0-dev libxcb-xinput-dev libxcb-icccm4-dev \ + libxcb-keysyms1-dev libxcb-cursor-dev libxcb-xfixes0-dev libxkbcommon-x11-dev \ + vulkan-tools +git lfs install +``` + +### Compiler: Clang 20 +Nabla on Linux requires **Clang 20** with LLD (Clang 18 encounters compiler crashes on `CAssetConverter.cpp`). GCC is not currently supported (no `CXX_GNU` profile). + +- **Option A (APT package)**: If `clang-20` is installed via APT: + ```bash + export CC=clang-20 + export CXX=clang++-20 + ``` +- **Option B (Standalone LLVM archive)**: If using a standalone LLVM/Clang release: + ```bash + export CC=/path/to/llvm-20/bin/clang + export CXX=/path/to/llvm-20/bin/clang++ + ``` + +### CMake (≥ 3.31) +The root `CMakeLists.txt` requires CMake version ≥ 3.31. Because Ubuntu 24.04 LTS defaults to CMake 3.28, install CMake 3.31+ or 4.x (tested with CMake 4.3.5) from the Kitware binary distribution or APT repository. Ensure the newer `cmake` executable is available in your `PATH`. + +### Vulkan SDK (1.4.x) +Nabla targets Vulkan 1.3+ / 1.4. While the engine vendors `Vulkan-Headers` and loads Vulkan dynamically via Volk, runtime execution and validation require a Vulkan 1.4 driver and the LunarG Vulkan SDK (tested with LunarG SDK **1.4.363.0**). + +Download and extract the SDK tarball, then activate the environment in your shell: +```bash +source /path/to/VulkanSDK/1.4.x.x/setup-env.sh +``` + +--- + +## 2. Git Submodules Initialization + +Nabla vendors dependencies (DXC, Boost, OpenEXR, glslang, shaderc, Vulkan-Headers, etc.) and example suites via git submodules. + +### Fresh Fork Submodule Hydration + +Because the repository fork uses relative submodule URLs in `.gitmodules`, git defaults to looking under the fork owner (`raydelto`) for all submodules. Two upstream submodules (`3rdparty/boost/superproject` and `docker/msvc-winsdk`) are hosted solely under upstream `Devsh-Graphics-Programming` and do not exist under `raydelto`. Furthermore, `Ditt-Reference-Scenes` is a private reference repository that must be excluded. + +Configure the explicit upstream URLs, exclude private scenes, configure protocol requirements (SSH vs. HTTPS), and initialize: + +```bash +# In your clone / worktree of raydelto/Nabla: +git checkout linux-port + +# 1. Override relative submodule URLs that exist only in upstream Devsh: +git config submodule."3rdparty/boost/superproject".url git@github.com:Devsh-Graphics-Programming/boost.git +git config submodule."docker/msvc-winsdk".url git@github.com:Devsh-Graphics-Programming/docker-nanoserver-msvc-winsdk.git + +# 2. Exclude private scenes and initialize recursively (use HTTPS rewrite if SSH keys are not set up): +git -c fetch.parallel=0 \ + -c url.https://github.com/.insteadOf=git@github.com: \ + -c submodule."Ditt-Reference-Scenes".update=none \ + submodule update --init --recursive +``` + +### Local Submodule Caching (Fast Path) + +When working across multiple local worktrees or checkouts, use the cached initialization helper to clone directly from an existing populated clone on disk without network downloads (~15 seconds): + +```bash +cmake/scripts/linux/init-submodules-cached.sh . +``` + +> **Note on examples fork pin**: Ensure the local cache clone has fetched the `raydelto` remote in its `examples_tests` submodule before hydrating: +> ```bash +> git -C /examples_tests fetch raydelto +> ``` +> This ensures that the integration pin `c337709e` from `raydelto/Nabla-Examples-and-Tests` is present in the cache's git object database. + +--- + +## 3. CMake Configuration + +Configure Nabla with the **Ninja** generator (required by the DirectX Shader Compiler sub-build on Linux) and Clang: + +```bash +cmake -S . -B build/linux-clang-release -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_LINKER_TYPE=LLD \ + -DNBL_NSC_MODE=SOURCE \ + -DNBL_BUILD_EXAMPLES=ON \ + -DNBL_PCH=ON \ + -DNBL_ENABLE_DOCKER_INTEGRATION=OFF \ + -DNBL_UPDATE_GIT_SUBMODULE=OFF \ + -D_NBL_JOBS_AMOUNT_=2 +``` + +### Key Configuration Flags: +- `-G Ninja`: **Mandatory**. The nested DXC sub-build enforces Ninja on Linux. +- `-DCMAKE_LINKER_TYPE=LLD`: Links using LLVM's `lld` linker. +- `-DNBL_NSC_MODE=SOURCE`: Builds the Nabla Shader Compiler (`nsc`) CLI from source. +- `-DNBL_BUILD_EXAMPLES=ON`: Enables building the examples and unit tests suite. +- `-DNBL_PCH=ON`: Enables precompiled headers. +- `-DNBL_ENABLE_DOCKER_INTEGRATION=OFF`: Disables Docker container packaging. +- `-DNBL_UPDATE_GIT_SUBMODULE=OFF`: Prevents CMake from re-updating pre-hydrated submodules. +- `-D_NBL_JOBS_AMOUNT_=2`: Throttles nested build job concurrency. + +--- + +## 4. Build Execution & Memory Throttling + +> [!WARNING] +> **RAM Constraint Warning**: Compiling Microsoft DXC (LLVM/Clang) and Boost templates consumes extensive memory. Always limit DXC compilation concurrency to 2 parallel threads (`-j2`). Unbounded parallel builds can exhaust system RAM and cause out-of-memory errors. + +### Step 1: Build DirectX Shader Compiler (`dxcompiler`) +```bash +cmake --build build/linux-clang-release -j2 --target dxcompiler +``` +Produces `build/linux-clang-release/3rdparty/dxc/build/lib/libdxcompiler.so`. + +### Step 2: Build Nabla Engine and NSC +```bash +cmake --build build/linux-clang-release -j4 --target nsc +``` +Produces `build/linux-clang-release/src/nbl/libNabla.so` and `tools/nsc/bin/nsc`. + +### Step 3: Run NSC Test Suite +Verify `nsc` compiler functionality and runtime discovery: +```bash +(cd build/linux-clang-release/tools/nsc && ctest --output-on-failure) +``` +Expect: `100% tests passed, 0 tests failed out of 6`. + +### Step 4: Build Examples and Unit Tests +```bash +cmake --build build/linux-clang-release -j4 --target \ + 01_hellocoresystemasset \ + 02_hellocompute \ + 21_lrucacheunittest \ + 23_arithmetic2unittest +``` +Executables are placed into `examples_tests//bin/`. + +--- + +## 5. Running and Validating Tests + +Before running GPU executables, make sure the Vulkan SDK environment is activated: +```bash +source /path/to/VulkanSDK/1.4.x.x/setup-env.sh +``` + +Optionally enable validation layers or select a specific GPU ICD: +```bash +# Enable Khronos validation layer and loader logging +export VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation +export VK_LOADER_DEBUG=layer,driver + +# Select GPU vendor ICD (examples): +# export VK_DRIVER_FILES=/usr/share/vulkan/icd.d/nvidia_icd.json # NVIDIA +# export VK_DRIVER_FILES=/usr/share/vulkan/icd.d/intel_icd.json # Intel Mesa +``` + +Execute each binary using a subshell `(cd ... && ./)` so the working directory of your shell is preserved: + +### 1. `01_HelloCoreSystemAsset` (VFS & Async Assets) +```bash +(cd examples_tests/01_HelloCoreSystemAsset/bin && ./01_hellocoresystemasset) +``` +- Tests VFS mounting, archive extraction, async I/O futures, and image encoding/decoding. +- Expected result: **Exit code 0** (clean pass on both NVIDIA and Intel setups). + +### 2. `02_HelloCompute` (Vulkan 1.4 Compute & BDA) +```bash +(cd examples_tests/02_HelloCompute/bin && ./02_hellocompute) +``` +- Compiles HLSL compute kernel at runtime via `libdxcompiler.so`, dispatches 524,288 threads using Buffer Device Addresses, synchronizes with timeline semaphores, and validates memory readback. +- Expected result: **Exit code 0** on both NVIDIA RTX and Intel UHD GPUs. + +### 3. `21_LRUCacheUnitTest` (Core Data Structures) +```bash +(cd examples_tests/21_LRUCacheUnitTest/bin && ./21_lrucacheunittest) +``` +- Stress tests `nbl::core::ResizableLRUCache` allocations and eviction callbacks. +- Expected result: **Exit code 0**. + +### 4. `23_Arithmetic2UnitTest` (GPU Workgroup & Subgroup Parallel Math) +```bash +(cd examples_tests/23_Arithmetic2UnitTest/bin && ./23_arithmetic2unittest) +``` +- Cross-validates GPU parallel reductions, inclusive scans, and exclusive scans against CPU ground truth across various subgroup and workgroup sizes. +- **NVIDIA GPU**: **Exit code 0** (~7 minutes runtime). +- **Intel UHD (Mesa ANV)**: Known driver bug — exits with **code 139** (segfault inside `libvulkan_intel.so` during `vkCreateComputePipelines` on native subgroup size 32 inclusive scan at workgroup size 64). Emulated subgroup sizes and native subgroup sizes 8/16 all pass. An upstream bug report to Mesa is pending (Troubleshooting Issue 21). From 50ea94ad1857c6744165e34f990c8159d0024c46 Mon Sep 17 00:00:00 2001 From: Raydelto Hernandez Date: Thu, 8 Oct 2026 01:20:09 -0300 Subject: [PATCH 6/9] NAB-9: XCB windowing and Vulkan XCB surface on Linux (#6) * NAB-9: Native XCB window manager and window on Linux IWindowManagerXcb / IWindowXcb are the Linux counterparts of the Win32 classes. One xcb_connection_t per manager and one event thread per manager (the same IAsyncQueueDispatcher pattern as CWindowManagerWin32). Window creation, registration and destruction run on that thread, so every IEventCallback call happens there and none happen after a window's destructor returns. Close (WM_DELETE_WINDOW) unmaps instead of destroying, because a Vulkan surface may still reference the X window. Reports shown/hidden, moved, resized, minimized, maximized (EWMH and ICCCM WM_STATE) and keyboard/mouse focus. Clipboard, cursor and input channels are the follow-up issue. Only libxcb is linked; no icccm/ewmh helpers. Linux builds define VK_USE_PLATFORM_XCB_KHR for Nabla and volk. Co-Authored-By: Claude Opus 5.5 Co-authored-by: multica-agent * NAB-9: VK_KHR_xcb_surface and CSurfaceVulkanXcb CSurfaceVulkanXcb (from an IWindowXcb) and CSurfaceVulkanXcbNative (from a connection + window pair) replace the Linux TODO in CSurfaceVulkan.h. CVulkanConnection enables VK_KHR_xcb_surface with ESM_SURFACE on Linux. The XCB native handle is a struct, not a pointer, so CSurface and CSurfaceNative return its address from getNativeWindowHandle(). Pointer handles (HWND, ANativeWindow*) are returned unchanged. Co-Authored-By: Claude Opus 5.5 Co-authored-by: multica-agent * NAB-9: Recreate the swapchain on OUT_OF_DATE in CSmoothResizeSurface handleOutOfDate() only retried the acquire, which returns nothing once the swapchain resources are invalidated, so the surface became irrecoverable. On X11 the driver reports OUT_OF_DATE as soon as the window size changes, which can be before the event thread calls explicitRecreateSwapchain, so a resize at high frame rates closed 08_HelloSwapchain. Recreate first, like CSimpleResizeSurface already does. Co-Authored-By: Claude Opus 5.5 Co-authored-by: multica-agent * NAB-9: Bump examples_tests to the XCB windowed examples raydelto/Nabla-Examples-and-Tests nab-9-xcb-windowing @ f6d66162: Linux branches in SimpleWindowedApplication, MonoWindowApplication and 08_HelloSwapchain, and an LP64 fix in 09_GeometryCreator. Co-Authored-By: Claude Opus 5.5 Co-authored-by: multica-agent * NAB-9: Create windows from XCB callbacks without deadlocking createWindow() queued its requests and waited on them even when called on the event thread, so creating a window from a callback (e.g. onWindowShown) stopped all event processing. On the event thread it now creates and registers the window directly; it already owns the window table. Lifetime contract: callbacks may create windows and drop the last reference to other windows. Dropping the last reference to the window whose callback is running stays forbidden, because IEventCallback updates its flags after the callback returns; this is now asserted. Adds the NBL_UI_XCB_WINDOW_MANAGER CTest (no GPU, skipped without an X server): creates a window from a callback, drops it from another callback, checks callbacks run off the main thread and the event thread survives. It fails with a 30 s deadlock on the previous code. Co-Authored-By: Claude Opus 5.5 Co-authored-by: multica-agent --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: multica-agent --- 3rdparty/CMakeLists.txt | 2 + examples_tests | 2 +- include/nbl/ui/IWindowManagerXcb.h | 36 ++ include/nbl/ui/IWindowXcb.h | 33 ++ include/nbl/ui/declarations.h | 2 +- include/nbl/video/CVulkanConnection.h | 2 + include/nbl/video/IAPIConnection.h | 2 +- include/nbl/video/surface/CSurfaceVulkan.h | 25 +- include/nbl/video/surface/ISurface.h | 12 +- .../video/utilities/CSmoothResizeSurface.h | 7 +- src/nbl/CMakeLists.txt | 17 +- src/nbl/ui/CWindowManagerXcb.cpp | 469 ++++++++++++++++++ src/nbl/ui/CWindowManagerXcb.h | 167 +++++++ src/nbl/ui/CWindowXcb.cpp | 170 +++++++ src/nbl/ui/CWindowXcb.h | 53 ++ src/nbl/ui/test/CMakeLists.txt | 10 + src/nbl/ui/test/CWindowManagerXcbTest.cpp | 141 ++++++ src/nbl/video/CSurfaceVulkan.cpp | 38 ++ src/nbl/video/CVulkanConnection.cpp | 2 + 19 files changed, 1181 insertions(+), 9 deletions(-) create mode 100644 include/nbl/ui/IWindowManagerXcb.h create mode 100644 include/nbl/ui/IWindowXcb.h create mode 100644 src/nbl/ui/CWindowManagerXcb.cpp create mode 100644 src/nbl/ui/CWindowManagerXcb.h create mode 100644 src/nbl/ui/CWindowXcb.cpp create mode 100644 src/nbl/ui/CWindowXcb.h create mode 100644 src/nbl/ui/test/CMakeLists.txt create mode 100644 src/nbl/ui/test/CWindowManagerXcbTest.cpp diff --git a/3rdparty/CMakeLists.txt b/3rdparty/CMakeLists.txt index 8e9efa2d7a..c635197b34 100755 --- a/3rdparty/CMakeLists.txt +++ b/3rdparty/CMakeLists.txt @@ -66,6 +66,8 @@ set(VOLK_PULL_IN_VULKAN OFF CACHE INTERNAL "" FORCE) # Needed to make sure vulka if (WIN32) set(VOLK_STATIC_DEFINES VK_USE_PLATFORM_WIN32_KHR) +elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set(VOLK_STATIC_DEFINES VK_USE_PLATFORM_XCB_KHR) endif() add_subdirectory(volk volk EXCLUDE_FROM_ALL) diff --git a/examples_tests b/examples_tests index c337709e48..f6d661623b 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit c337709e48b235225483f8877508c67ef140713a +Subproject commit f6d661623b1f569e1671081edbf6b09bed7e7ae6 diff --git a/include/nbl/ui/IWindowManagerXcb.h b/include/nbl/ui/IWindowManagerXcb.h new file mode 100644 index 0000000000..ca6a8bca2c --- /dev/null +++ b/include/nbl/ui/IWindowManagerXcb.h @@ -0,0 +1,36 @@ +#ifndef _NBL_UI_I_WINDOWMANAGER_XCB_INCLUDED_ +#define _NBL_UI_I_WINDOWMANAGER_XCB_INCLUDED_ + +#include "nbl/ui/IWindowManager.h" + +#ifdef _NBL_PLATFORM_LINUX_ +namespace nbl::ui +{ + +// Native X11 window manager built on XCB, also works under XWayland. +// +// Threading and lifetime rules: +// - The manager owns one `xcb_connection_t`, opened by `create()` (from `$DISPLAY`) and closed when the manager is destroyed. +// `create()` returns nullptr if no X server can be reached. +// - One dedicated thread per manager reads all X events. Every `IWindow::IEventCallback` of every window created by this +// manager is called on that thread, never on the thread that created the window, and never concurrently with itself. +// - Windows hold a reference to their manager, so the manager (and its connection) outlives all its windows. +// - Window creation and destruction are processed on the event thread, so once a window's destructor returns no more +// callbacks will be made for it. +// - Callbacks may create windows, and may drop the last reference to any window other than the one they were raised for. +// Dropping the last reference to the window whose callback is running is not allowed (asserted in debug builds), +// because `IWindow::IEventCallback` still updates that window's flags after the callback returns. +// Defer it, e.g. by flagging the window and releasing it from your own thread. +// - `onWindowClosed` is raised for `WM_DELETE_WINDOW`. If it returns true the window is unmapped (hidden), not destroyed. +// The X window lives until the last reference to the `IWindow` (held by e.g. a Vulkan surface) is dropped. +// - All other manager and window methods may be called from any thread. They only send requests to the X server, +// the resulting state changes are reported back through the callbacks. +class IWindowManagerXcb : public IWindowManager +{ + public: + NBL_API2 static core::smart_refctd_ptr create(); +}; + +} +#endif +#endif diff --git a/include/nbl/ui/IWindowXcb.h b/include/nbl/ui/IWindowXcb.h new file mode 100644 index 0000000000..596bc5318d --- /dev/null +++ b/include/nbl/ui/IWindowXcb.h @@ -0,0 +1,33 @@ +#ifndef _NBL_I_WINDOW_XCB_H_INCLUDED_ +#define _NBL_I_WINDOW_XCB_H_INCLUDED_ + +#include "nbl/ui/IWindowManagerXcb.h" + +#ifdef _NBL_PLATFORM_LINUX_ +// forward declare, so the public headers don't need +struct xcb_connection_t; + +namespace nbl::ui +{ + +class NBL_API2 IWindowXcb : public IWindow +{ + public: + // An X11 window is only meaningful together with the connection it was created on + struct native_handle_t + { + xcb_connection_t* connection = nullptr; + uint32_t window = 0u; // `xcb_window_t` + }; + virtual const native_handle_t& getNativeHandle() const = 0; + + protected: + inline IWindowXcb(SCreationParams&& params) : IWindow(std::move(params)) {} + virtual ~IWindowXcb() = default; +}; + +} + +#endif + +#endif diff --git a/include/nbl/ui/declarations.h b/include/nbl/ui/declarations.h index 7b4936d998..986ded2303 100644 --- a/include/nbl/ui/declarations.h +++ b/include/nbl/ui/declarations.h @@ -13,7 +13,7 @@ #elif defined(_NBL_BUILD_WITH_WAYLAND) && defined(_NBL_TEST_WAYLAND) # include "nbl/ui/CWindowManagerWayland.h" #elif defined(_NBL_PLATFORM_LINUX_) -# include "nbl/ui/IWindowManager.h" // no native window manager yet, only the interface +# include "nbl/ui/IWindowXcb.h" #endif // TODO more platforms (android) // clipboards diff --git a/include/nbl/video/CVulkanConnection.h b/include/nbl/video/CVulkanConnection.h index f95c11e6b7..1cf537682c 100644 --- a/include/nbl/video/CVulkanConnection.h +++ b/include/nbl/video/CVulkanConnection.h @@ -6,6 +6,8 @@ #if defined(_NBL_PLATFORM_WINDOWS_) # include "nbl/ui/IWindowWin32.h" +#elif defined(_NBL_PLATFORM_LINUX_) +# include "nbl/ui/IWindowXcb.h" #endif #include diff --git a/include/nbl/video/IAPIConnection.h b/include/nbl/video/IAPIConnection.h index 2fb1f28fed..09bdaf8bc3 100644 --- a/include/nbl/video/IAPIConnection.h +++ b/include/nbl/video/IAPIConnection.h @@ -25,7 +25,7 @@ class NBL_API2 IAPIConnection : public core::IReferenceCounted // for example you need to enable E_SWAPCHAIN_MODE::ESM_SURFACE in order for the physical device to report support in SPhysicalDeviceFeatures::swapchainMode struct SFeatures { - // VK_KHR_surface, VK_KHR_win32_surface, VK_KHR_display(TODO) + // VK_KHR_surface, VK_KHR_win32_surface or VK_KHR_xcb_surface, VK_KHR_display(TODO) core::bitflag swapchainMode = E_SWAPCHAIN_MODE::ESM_NONE; // VK_LAYER_KHRONOS_validation (instance layer) diff --git a/include/nbl/video/surface/CSurfaceVulkan.h b/include/nbl/video/surface/CSurfaceVulkan.h index ddf0e39165..03a2e52275 100644 --- a/include/nbl/video/surface/CSurfaceVulkan.h +++ b/include/nbl/video/surface/CSurfaceVulkan.h @@ -58,7 +58,30 @@ class NBL_API2 CSurfaceVulkanWin32Native final : public CSurfaceNative +{ + using this_t = CSurfaceVulkanXcb; + using base_t = CSurface; + public: + inline CSurfaceVulkanXcb(core::smart_refctd_ptr&& window, core::smart_refctd_ptr&& api, VkSurfaceKHR surf) : + base_t(std::move(window), std::move(api), surf) {} + + static core::smart_refctd_ptr create(core::smart_refctd_ptr&& api, core::smart_refctd_ptr&& window); +}; + +class NBL_API2 CSurfaceVulkanXcbNative final : public CSurfaceNative +{ + using this_t = CSurfaceVulkanXcbNative; + using base_t = CSurfaceNative; + public: + inline CSurfaceVulkanXcbNative(core::smart_refctd_ptr&& api, const typename ui::IWindowXcb::native_handle_t& handle, VkSurfaceKHR surf) : + base_t(handle, std::move(api), surf) + { + } + + // the caller keeps the connection open and the window alive for the lifetime of the surface + static core::smart_refctd_ptr create(core::smart_refctd_ptr&& api, const ui::IWindowXcb::native_handle_t& handle); +}; #elif defined _NBL_PLATFORM_ANDROID_ // TODO: later, not this week #endif diff --git a/include/nbl/video/surface/ISurface.h b/include/nbl/video/surface/ISurface.h index 2a1b561ebc..9abbb67394 100644 --- a/include/nbl/video/surface/ISurface.h +++ b/include/nbl/video/surface/ISurface.h @@ -115,9 +115,14 @@ class CSurface : public ImmediateBase } inline const window_t* getWindow() const {return const_cast(const_cast(this)->getWindow());} + // handles which aren't pointers (e.g. XCB's connection and window pair) are returned by address inline const void* getNativeWindowHandle() const override final { - return m_window->getNativeHandle(); + const auto& handle = m_window->getNativeHandle(); + if constexpr (std::is_pointer_v>) + return handle; + else + return &handle; } protected: @@ -138,7 +143,10 @@ class CSurfaceNative : public ImmediateBase inline const void* getNativeWindowHandle() const override final { - return m_handle; + if constexpr (std::is_pointer_v) + return m_handle; + else + return &m_handle; } protected: diff --git a/include/nbl/video/utilities/CSmoothResizeSurface.h b/include/nbl/video/utilities/CSmoothResizeSurface.h index 017f1cd2d4..f8a23d2538 100644 --- a/include/nbl/video/utilities/CSmoothResizeSurface.h +++ b/include/nbl/video/utilities/CSmoothResizeSurface.h @@ -281,8 +281,11 @@ class NBL_API2 ISmoothResizeSurface : public ISimpleManagedSurface // handlers for acquisition exceptions (will get called under mutexes) inline SAcquireResult handleOutOfDate() override final { - // try again, will re-create swapchain - return ISimpleManagedSurface::acquireNextImage(); + // The swapchain can go out of date before the window event that would have triggered `explicitRecreateSwapchain` gets processed, + // e.g. on X11 the event thread races the render thread. Recreate here, `ISimpleManagedSurface::acquireNextImage` won't use invalidated resources. + if (recreateSwapchain()) + return ISimpleManagedSurface::acquireNextImage(); + return {}; } // diff --git a/src/nbl/CMakeLists.txt b/src/nbl/CMakeLists.txt index 139da9d8bc..e46ed86137 100644 --- a/src/nbl/CMakeLists.txt +++ b/src/nbl/CMakeLists.txt @@ -157,6 +157,8 @@ set(NBL_SYSTEM_SOURCES set(NBL_UI_SOURCES ui/CWindowWin32.cpp ui/CWindowManagerWin32.cpp + ui/CWindowXcb.cpp + ui/CWindowManagerXcb.cpp ui/CWindowManagerAndroid.cpp ui/CGraphicalApplicationAndroid.cpp ) @@ -655,6 +657,11 @@ if (UNIX AND NOT ANDROID AND NOT APPLE) ${X11_Xrandr_INCLUDE_PATH} ${X11_xf86vmode_INCLUDE_PATH} ) + # CWindowManagerXcb, and the VK_KHR_xcb_surface types in the public Vulkan headers + if(NOT X11_xcb_FOUND) + message(FATAL_ERROR "libxcb development files are required for the X11 window manager (libxcb1-dev)") + endif() + target_link_libraries(Nabla PRIVATE X11::xcb) endif() # Vulkan @@ -664,7 +671,10 @@ list(APPEND PUBLIC_BUILD_INCLUDE_DIRS ) target_link_libraries(Nabla PRIVATE volk) # volk is part of public interface headers in Nabla -target_compile_definitions(Nabla PUBLIC $<$:VK_USE_PLATFORM_WIN32_KHR>) +target_compile_definitions(Nabla PUBLIC + $<$:VK_USE_PLATFORM_WIN32_KHR> + $<$:VK_USE_PLATFORM_XCB_KHR> +) list(APPEND PUBLIC_BUILD_INCLUDE_DIRS # this should be PRIVATE, but things from /src (or /source) are sometimes included in things in /include and so examples have to put source dirs into theirs Include Path @@ -981,3 +991,8 @@ source_group(TREE "${NBL_ROOT_PATH}" PREFIX "Source Files" FILES ${NABLA_SOURCE_FILES} ) + +# XCB window manager regression test (NAB-9) +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_subdirectory(ui/test) +endif() diff --git a/src/nbl/ui/CWindowManagerXcb.cpp b/src/nbl/ui/CWindowManagerXcb.cpp new file mode 100644 index 0000000000..de9fb33298 --- /dev/null +++ b/src/nbl/ui/CWindowManagerXcb.cpp @@ -0,0 +1,469 @@ +#include "nbl/ui/CWindowManagerXcb.h" +#include "nbl/ui/CWindowXcb.h" + +#ifdef _NBL_PLATFORM_LINUX_ +#include + +#include +#include +#include +#include + +using namespace nbl; +using namespace nbl::ui; + +namespace +{ +// ICCCM and Motif constants, we don't depend on xcb-icccm for a handful of them +constexpr uint32_t WM_STATE_NORMAL = 1u; +constexpr uint32_t WM_STATE_ICONIC = 3u; + +constexpr uint32_t WM_HINTS_INPUT = 1u<<0; +constexpr uint32_t WM_HINTS_STATE = 1u<<1; + +constexpr uint32_t WM_SIZE_HINT_US_POSITION = 1u<<0; +constexpr uint32_t WM_SIZE_HINT_P_POSITION = 1u<<2; +constexpr uint32_t WM_SIZE_HINT_P_MIN_SIZE = 1u<<4; +constexpr uint32_t WM_SIZE_HINT_P_MAX_SIZE = 1u<<5; + +constexpr uint32_t MWM_HINTS_FUNCTIONS = 1u<<0; +constexpr uint32_t MWM_HINTS_DECORATIONS = 1u<<1; +constexpr uint32_t MWM_FUNC_RESIZE = 1u<<1; +constexpr uint32_t MWM_FUNC_MOVE = 1u<<2; +constexpr uint32_t MWM_FUNC_MINIMIZE = 1u<<3; +constexpr uint32_t MWM_FUNC_MAXIMIZE = 1u<<4; +constexpr uint32_t MWM_FUNC_CLOSE = 1u<<5; + +constexpr uint32_t NET_WM_STATE_ADD = 1u; +constexpr uint32_t NET_WM_SOURCE_APPLICATION = 1u; + +// the timeout also bounds how long a create/destroy request can wait for the event thread, same as the Win32 timer +constexpr int EventPollTimeoutMS = 8; +} + +core::smart_refctd_ptr IWindowManagerXcb::create() +{ + int screenIx = 0; + xcb_connection_t* connection = xcb_connect(nullptr,&screenIx); + if (xcb_connection_has_error(connection)) + { + xcb_disconnect(connection); + return nullptr; + } + + xcb_screen_t* screen = nullptr; + for (auto it=xcb_setup_roots_iterator(xcb_get_setup(connection)); it.rem; screenIx--,xcb_screen_next(&it)) + if (screenIx==0) + { + screen = it.data; + break; + } + if (!screen) + { + xcb_disconnect(connection); + return nullptr; + } + + CWindowManagerXcb::SAtoms atoms; + { + struct SAtomRequest + { + xcb_atom_t* dst; + const char* name; + xcb_intern_atom_cookie_t cookie; + }; + #define NBL_XCB_ATOM(NAME) SAtomRequest{&atoms.NAME,#NAME,{}} + SAtomRequest requests[] = { + NBL_XCB_ATOM(WM_PROTOCOLS), + NBL_XCB_ATOM(WM_DELETE_WINDOW), + NBL_XCB_ATOM(WM_STATE), + NBL_XCB_ATOM(WM_CHANGE_STATE), + NBL_XCB_ATOM(UTF8_STRING), + NBL_XCB_ATOM(_NET_WM_NAME), + NBL_XCB_ATOM(_NET_WM_STATE), + NBL_XCB_ATOM(_NET_WM_STATE_HIDDEN), + NBL_XCB_ATOM(_NET_WM_STATE_MAXIMIZED_VERT), + NBL_XCB_ATOM(_NET_WM_STATE_MAXIMIZED_HORZ), + NBL_XCB_ATOM(_NET_WM_STATE_FULLSCREEN), + NBL_XCB_ATOM(_NET_WM_STATE_ABOVE), + NBL_XCB_ATOM(_MOTIF_WM_HINTS) + }; + #undef NBL_XCB_ATOM + // send all the requests before waiting for any reply + for (auto& request : requests) + request.cookie = xcb_intern_atom(connection,0,strlen(request.name),request.name); + bool success = true; + for (auto& request : requests) + { + xcb_intern_atom_reply_t* reply = xcb_intern_atom_reply(connection,request.cookie,nullptr); + success = success && reply; + *request.dst = reply ? reply->atom:XCB_ATOM_NONE; + free(reply); + } + if (!success) + { + xcb_disconnect(connection); + return nullptr; + } + } + + return core::make_smart_refctd_ptr(connection,screen,atoms); +} + +CWindowManagerXcb::CWindowManagerXcb(xcb_connection_t* connection, xcb_screen_t* screen, const SAtoms& atoms) : + m_connection{connection}, m_screen(screen), m_atoms(atoms), m_eventThread(this) +{ +} + +IWindowManager::SDisplayInfo CWindowManagerXcb::getPrimaryDisplayInfo() const +{ + // TODO: use RandR to get the primary output, the X screen spans all the monitors + SDisplayInfo info{}; + info.x = 0; + info.y = 0; + info.resX = m_screen->width_in_pixels; + info.resY = m_screen->height_in_pixels; + info.name = "X11 screen"; + return info; +} + +core::smart_refctd_ptr CWindowManagerXcb::createWindow(IWindow::SCreationParams&& creationParams) +{ + // same normalization as Win32 + if (creationParams.flags.hasFlags(IWindow::ECF_CAN_RESIZE) || creationParams.flags.hasFlags(IWindow::ECF_CAN_MAXIMIZE)) + creationParams.flags |= IWindow::ECF_RESIZABLE; + // X11 rejects zero sized windows + creationParams.width = std::max(creationParams.width,1u); + creationParams.height = std::max(creationParams.height,1u); + + const bool map = !creationParams.flags.hasFlags(IWindow::ECF_HIDDEN); + // Called from a callback, the event thread can't wait on its own queue, but it already owns the window table + if (m_eventThread.isEventThread()) + { + const xcb_window_t nativeWindow = createNativeWindow(creationParams); + if (nativeWindow==XCB_WINDOW_NONE) + return nullptr; + auto window = core::make_smart_refctd_ptr(std::move(creationParams),core::smart_refctd_ptr(this),nativeWindow); + registerWindow(window.get(),map); + return window; + } + + CAsyncQueue::future_t future; + m_eventThread.request(&future,SRequestParams_CreateWindow{.params=&creationParams}); + auto nativeWindow = future.acquire(); + if (!nativeWindow || *nativeWindow==XCB_WINDOW_NONE) + return nullptr; + + auto window = core::make_smart_refctd_ptr(std::move(creationParams),core::smart_refctd_ptr(this),*nativeWindow); + // the window must be known to the event thread before it's mapped, otherwise we'd lose the first events + CAsyncQueue::future_t registered; + m_eventThread.request(®istered,SRequestParams_RegisterWindow{.window=window.get(),.map=map}); + registered.wait(); + return window; +} + +void CWindowManagerXcb::destroyWindow(IWindow* wnd) +{ + const auto nativeWindow = static_cast(wnd)->getNativeHandle().window; + // the last reference to some other window got dropped by a callback + if (m_eventThread.isEventThread()) + { + // The window whose event is being dispatched can't die here, `IWindow::IEventCallback` still writes its flags after the callback returns + assert(wnd!=m_dispatchingWindow); + unregisterWindow(nativeWindow); + return; + } + CAsyncQueue::future_t future; + m_eventThread.request(&future,SRequestParams_DestroyWindow{.window=nativeWindow}); + future.wait(); +} + +void CWindowManagerXcb::setSizeHints(xcb_window_t window, int32_t x, int32_t y, uint32_t width, uint32_t height, bool fixedSize) const +{ + // ICCCM `WM_SIZE_HINTS`: flags, 4 obsolete fields, min, max, increments, min/max aspect, base size, gravity + uint32_t hints[18] = {}; + hints[0] = WM_SIZE_HINT_US_POSITION|WM_SIZE_HINT_P_POSITION; + hints[1] = static_cast(x); + hints[2] = static_cast(y); + if (fixedSize) + { + hints[0] |= WM_SIZE_HINT_P_MIN_SIZE|WM_SIZE_HINT_P_MAX_SIZE; + hints[5] = hints[7] = width; + hints[6] = hints[8] = height; + } + xcb_change_property(m_connection.handle,XCB_PROP_MODE_REPLACE,window,XCB_ATOM_WM_NORMAL_HINTS,XCB_ATOM_WM_SIZE_HINTS,32,18,hints); +} + +bool CWindowManagerXcb::setWindowSize_impl(IWindow* window, const uint32_t width, const uint32_t height) +{ + const auto nativeWindow = static_cast(window)->getNativeHandle().window; + // a window the user can't resize has its min and max size locked, move both + if (!window->isResizable()) + setSizeHints(nativeWindow,window->getX(),window->getY(),width,height,true); + const uint32_t values[2] = {std::max(width,1u),std::max(height,1u)}; + xcb_configure_window(m_connection.handle,nativeWindow,XCB_CONFIG_WINDOW_WIDTH|XCB_CONFIG_WINDOW_HEIGHT,values); + xcb_flush(m_connection.handle); + return true; +} + +bool CWindowManagerXcb::setWindowPosition_impl(IWindow* window, const int32_t x, const int32_t y) +{ + const uint32_t values[2] = {static_cast(x),static_cast(y)}; + xcb_configure_window(m_connection.handle,static_cast(window)->getNativeHandle().window,XCB_CONFIG_WINDOW_X|XCB_CONFIG_WINDOW_Y,values); + xcb_flush(m_connection.handle); + return true; +} + +bool CWindowManagerXcb::setWindowVisible_impl(IWindow* window, const bool visible) +{ + const auto nativeWindow = static_cast(window)->getNativeHandle().window; + if (visible) + xcb_map_window(m_connection.handle,nativeWindow); + else + xcb_unmap_window(m_connection.handle,nativeWindow); + xcb_flush(m_connection.handle); + return true; +} + +bool CWindowManagerXcb::setWindowMaximized_impl(IWindow* window, const bool maximized) +{ + const auto nativeWindow = static_cast(window)->getNativeHandle().window; + if (maximized) + sendToRoot(nativeWindow,m_atoms._NET_WM_STATE,{NET_WM_STATE_ADD,m_atoms._NET_WM_STATE_MAXIMIZED_VERT,m_atoms._NET_WM_STATE_MAXIMIZED_HORZ,NET_WM_SOURCE_APPLICATION,0}); + else // `IWindowManager::minimize` lands here + sendToRoot(nativeWindow,m_atoms.WM_CHANGE_STATE,{WM_STATE_ICONIC,0,0,0,0}); + xcb_flush(m_connection.handle); + return true; +} + +void CWindowManagerXcb::sendToRoot(xcb_window_t window, xcb_atom_t type, const uint32_t (&data)[5]) const +{ + // `xcb_send_event` always copies 32 bytes + xcb_client_message_event_t ev = {}; + ev.response_type = XCB_CLIENT_MESSAGE; + ev.format = 32; + ev.window = window; + ev.type = type; + std::copy_n(data,5,ev.data.data32); + xcb_send_event(m_connection.handle,0,m_screen->root,XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT|XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY,reinterpret_cast(&ev)); +} + +xcb_window_t CWindowManagerXcb::createNativeWindow(const IWindow::SCreationParams& params) +{ + auto* const connection = m_connection.handle; + const auto flags = params.flags; + + const xcb_window_t window = xcb_generate_id(connection); + { + // no background, so the server doesn't clear what Vulkan presented while resizing + const uint32_t valueMask = XCB_CW_BACK_PIXMAP|XCB_CW_EVENT_MASK; + const uint32_t values[2] = { + XCB_BACK_PIXMAP_NONE, + XCB_EVENT_MASK_STRUCTURE_NOTIFY|XCB_EVENT_MASK_PROPERTY_CHANGE|XCB_EVENT_MASK_FOCUS_CHANGE|XCB_EVENT_MASK_ENTER_WINDOW|XCB_EVENT_MASK_LEAVE_WINDOW|XCB_EVENT_MASK_EXPOSURE + }; + const auto cookie = xcb_create_window_checked( + connection,XCB_COPY_FROM_PARENT,window,m_screen->root, + static_cast(params.x),static_cast(params.y),static_cast(params.width),static_cast(params.height), + 0,XCB_WINDOW_CLASS_INPUT_OUTPUT,m_screen->root_visual,valueMask,values + ); + if (xcb_generic_error_t* error=xcb_request_check(connection,cookie)) + { + free(error); + return XCB_WINDOW_NONE; + } + } + + // ask to be told about the close button instead of getting killed + xcb_change_property(connection,XCB_PROP_MODE_REPLACE,window,m_atoms.WM_PROTOCOLS,XCB_ATOM_ATOM,32,1,&m_atoms.WM_DELETE_WINDOW); + { + constexpr char wmClass[] = "nabla\0Nabla"; + xcb_change_property(connection,XCB_PROP_MODE_REPLACE,window,XCB_ATOM_WM_CLASS,XCB_ATOM_STRING,8,sizeof(wmClass),wmClass); + } + xcb_change_property(connection,XCB_PROP_MODE_REPLACE,window,XCB_ATOM_WM_NAME,XCB_ATOM_STRING,8,params.windowCaption.size(),params.windowCaption.data()); + xcb_change_property(connection,XCB_PROP_MODE_REPLACE,window,m_atoms._NET_WM_NAME,m_atoms.UTF8_STRING,8,params.windowCaption.size(),params.windowCaption.data()); + + // window managers ignore the position of `xcb_create_window` unless we insist + setSizeHints(window,params.x,params.y,params.width,params.height,!flags.hasFlags(IWindow::ECF_CAN_RESIZE)); + { + // ICCCM `WM_HINTS`: flags, input, initial_state, icon pixmap, icon window, icon x/y, icon mask, window group + uint32_t hints[9] = {}; + hints[0] = WM_HINTS_INPUT|WM_HINTS_STATE; + hints[1] = 1u; + hints[2] = flags.hasFlags(IWindow::ECF_MINIMIZED) ? WM_STATE_ICONIC:WM_STATE_NORMAL; + xcb_change_property(connection,XCB_PROP_MODE_REPLACE,window,XCB_ATOM_WM_HINTS,XCB_ATOM_WM_HINTS,32,9,hints); + } + { + // Motif hints are what X11 window managers still honour for decorations and the title bar buttons + uint32_t functions = MWM_FUNC_MOVE|MWM_FUNC_CLOSE; + if (flags.hasFlags(IWindow::ECF_CAN_RESIZE)) + functions |= MWM_FUNC_RESIZE; + if (flags.hasFlags(IWindow::ECF_CAN_MINIMIZE)) + functions |= MWM_FUNC_MINIMIZE; + if (flags.hasFlags(IWindow::ECF_CAN_MAXIMIZE)) + functions |= MWM_FUNC_MAXIMIZE; + const bool decorated = !flags.hasFlags(IWindow::ECF_BORDERLESS) && !flags.hasFlags(IWindow::ECF_FULLSCREEN); + // flags, functions, decorations, input mode, status + const uint32_t hints[5] = {MWM_HINTS_FUNCTIONS|MWM_HINTS_DECORATIONS,functions,decorated ? 1u:0u,0u,0u}; + xcb_change_property(connection,XCB_PROP_MODE_REPLACE,window,m_atoms._MOTIF_WM_HINTS,m_atoms._MOTIF_WM_HINTS,32,5,hints); + } + { + // EWMH initial state, only read by the window manager when the window gets mapped + core::vector state; + if (flags.hasFlags(IWindow::ECF_FULLSCREEN)) + state.push_back(m_atoms._NET_WM_STATE_FULLSCREEN); + if (flags.hasFlags(IWindow::ECF_ALWAYS_ON_TOP)) + state.push_back(m_atoms._NET_WM_STATE_ABOVE); + if (flags.hasFlags(IWindow::ECF_MAXIMIZED)) + { + state.push_back(m_atoms._NET_WM_STATE_MAXIMIZED_VERT); + state.push_back(m_atoms._NET_WM_STATE_MAXIMIZED_HORZ); + } + if (!state.empty()) + xcb_change_property(connection,XCB_PROP_MODE_REPLACE,window,m_atoms._NET_WM_STATE,XCB_ATOM_ATOM,32,state.size(),state.data()); + } + xcb_flush(connection); + return window; +} + +void CWindowManagerXcb::registerWindow(CWindowXcb* window, const bool map) +{ + const auto nativeWindow = window->getNativeHandle().window; + m_windows[nativeWindow] = window; + if (map) + { + xcb_map_window(m_connection.handle,nativeWindow); + xcb_flush(m_connection.handle); + } +} + +void CWindowManagerXcb::unregisterWindow(xcb_window_t window) +{ + m_windows.erase(window); + xcb_destroy_window(m_connection.handle,window); + xcb_flush(m_connection.handle); +} + +CWindowXcb* CWindowManagerXcb::findWindow(xcb_window_t window) const +{ + auto found = m_windows.find(window); + return found!=m_windows.end() ? found->second:nullptr; +} + +void CWindowManagerXcb::dispatchEvent(const xcb_generic_event_t* event) +{ + // the top bit flags events sent with `SendEvent`, e.g. by the window manager + switch (event->response_type&0x7fu) + { + case XCB_CLIENT_MESSAGE: + { + const auto* ev = reinterpret_cast(event); + if (ev->type==m_atoms.WM_PROTOCOLS && ev->format==32 && ev->data.data32[0]==m_atoms.WM_DELETE_WINDOW) + if (auto* window=beginDispatch(ev->window)) + window->onDeleteRequested(); + break; + } + case XCB_CONFIGURE_NOTIFY: + { + const auto* ev = reinterpret_cast(event); + if (auto* window=beginDispatch(ev->window)) + window->onConfigured(ev); + break; + } + case XCB_MAP_NOTIFY: + { + const auto* ev = reinterpret_cast(event); + if (auto* window=beginDispatch(ev->window)) + window->onMapped(); + break; + } + case XCB_UNMAP_NOTIFY: + { + const auto* ev = reinterpret_cast(event); + if (auto* window=beginDispatch(ev->window)) + window->onUnmapped(); + break; + } + case XCB_PROPERTY_NOTIFY: + { + const auto* ev = reinterpret_cast(event); + if (ev->atom==m_atoms._NET_WM_STATE || ev->atom==m_atoms.WM_STATE) + if (auto* window=beginDispatch(ev->window)) + window->updateState(); + break; + } + case XCB_FOCUS_IN: [[fallthrough]]; + case XCB_FOCUS_OUT: + { + const auto* ev = reinterpret_cast(event); + // pointer focus details are about the window under the pointer, not ours + if (ev->detail!=XCB_NOTIFY_DETAIL_POINTER) + if (auto* window=beginDispatch(ev->event)) + window->onFocusChanged(true,(event->response_type&0x7fu)==XCB_FOCUS_IN); + break; + } + case XCB_ENTER_NOTIFY: [[fallthrough]]; + case XCB_LEAVE_NOTIFY: + { + const auto* ev = reinterpret_cast(event); + if (ev->detail!=XCB_NOTIFY_DETAIL_INFERIOR) + if (auto* window=beginDispatch(ev->event)) + window->onFocusChanged(false,(event->response_type&0x7fu)==XCB_ENTER_NOTIFY); + break; + } + default: + // errors (response type 0) of unchecked requests also land here, nothing we can do about them + break; + } +} + +void CWindowManagerXcb::CAsyncQueue::background_work() +{ + auto* const connection = m_manager->m_connection.handle; + // a dead connection would make `poll` return immediately forever + if (xcb_connection_has_error(connection)) + { + std::this_thread::sleep_for(std::chrono::milliseconds(EventPollTimeoutMS)); + return; + } + + const auto dispatchAll = [&]() -> bool + { + bool any = false; + // also returns events which got queued while we waited on a reply + while (xcb_generic_event_t* event=xcb_poll_for_event(connection)) + { + m_manager->dispatchEvent(event); + m_manager->m_dispatchingWindow = nullptr; + free(event); + any = true; + } + return any; + }; + if (dispatchAll()) + return; + + pollfd pfd = {}; + pfd.fd = xcb_get_file_descriptor(connection); + pfd.events = POLLIN; + if (poll(&pfd,1,EventPollTimeoutMS)>0) + dispatchAll(); +} + +void CWindowManagerXcb::CAsyncQueue::process_request(base_t::future_base_t* _future_base, SRequest& req) +{ + std::visit([&](auto& params)->void + { + using params_t = std::remove_reference_t; + using retval_t = typename params_t::retval_t; + auto* retval = base_t::future_storage_cast(_future_base); + if constexpr (std::is_same_v) + retval->construct(m_manager->createNativeWindow(*params.params)); + else if constexpr (std::is_same_v) + m_manager->registerWindow(params.window,params.map); + else if constexpr (std::is_same_v) + m_manager->unregisterWindow(params.window); + else + assert(false); + },req.params); +} +#endif diff --git a/src/nbl/ui/CWindowManagerXcb.h b/src/nbl/ui/CWindowManagerXcb.h new file mode 100644 index 0000000000..e05f05da7d --- /dev/null +++ b/src/nbl/ui/CWindowManagerXcb.h @@ -0,0 +1,167 @@ +#ifndef _NBL_UI_C_WINDOWMANAGER_XCB_INCLUDED_ +#define _NBL_UI_C_WINDOWMANAGER_XCB_INCLUDED_ + +#include "nbl/ui/IWindowManagerXcb.h" +#include "nbl/ui/IWindowXcb.h" + +#ifdef _NBL_PLATFORM_LINUX_ +#include + +#include +#include + +namespace nbl::ui +{ + +class CWindowXcb; + +// See `IWindowManagerXcb` for the threading and lifetime rules +class NBL_API2 CWindowManagerXcb final : public IWindowManagerXcb +{ + public: + struct SAtoms + { + xcb_atom_t WM_PROTOCOLS; + xcb_atom_t WM_DELETE_WINDOW; + xcb_atom_t WM_STATE; + xcb_atom_t WM_CHANGE_STATE; + xcb_atom_t UTF8_STRING; + xcb_atom_t _NET_WM_NAME; + xcb_atom_t _NET_WM_STATE; + xcb_atom_t _NET_WM_STATE_HIDDEN; + xcb_atom_t _NET_WM_STATE_MAXIMIZED_VERT; + xcb_atom_t _NET_WM_STATE_MAXIMIZED_HORZ; + xcb_atom_t _NET_WM_STATE_FULLSCREEN; + xcb_atom_t _NET_WM_STATE_ABOVE; + xcb_atom_t _MOTIF_WM_HINTS; + }; + + // takes ownership of the connection + CWindowManagerXcb(xcb_connection_t* connection, xcb_screen_t* screen, const SAtoms& atoms); + + SDisplayInfo getPrimaryDisplayInfo() const override final; + + core::smart_refctd_ptr createWindow(IWindow::SCreationParams&& creationParams) override final; + + void destroyWindow(IWindow* wnd) override final; + + inline xcb_connection_t* getConnection() const {return m_connection.handle;} + inline const SAtoms& getAtoms() const {return m_atoms;} + inline xcb_window_t getRootWindow() const {return m_screen->root;} + + // ICCCM `WM_NORMAL_HINTS`, used to lock the size of windows the user can't resize + void setSizeHints(xcb_window_t window, int32_t x, int32_t y, uint32_t width, uint32_t height, bool fixedSize) const; + + protected: + ~CWindowManagerXcb() override = default; + + bool setWindowSize_impl(IWindow* window, const uint32_t width, const uint32_t height) override; + bool setWindowPosition_impl(IWindow* window, const int32_t x, const int32_t y) override; + inline bool setWindowRotation_impl(IWindow* window, const bool landscape) override {return false;} + bool setWindowVisible_impl(IWindow* window, const bool visible) override; + bool setWindowMaximized_impl(IWindow* window, const bool maximized) override; + + private: + // EWMH and ICCCM requests to the window manager are client messages sent to the root window + void sendToRoot(xcb_window_t window, xcb_atom_t type, const uint32_t (&data)[5]) const; + + // everything below runs on the event thread + xcb_window_t createNativeWindow(const IWindow::SCreationParams& params); + void registerWindow(CWindowXcb* window, const bool map); + void unregisterWindow(xcb_window_t window); + void dispatchEvent(const xcb_generic_event_t* event); + CWindowXcb* findWindow(xcb_window_t window) const; + // `findWindow` that also remembers which window's callbacks are about to run + inline CWindowXcb* beginDispatch(xcb_window_t window) + { + CWindowXcb* const found = findWindow(window); + m_dispatchingWindow = found; + return found; + } + + // Declared first so it's destroyed last, after the event thread has stopped + struct SConnection + { + inline ~SConnection() + { + if (handle) + xcb_disconnect(handle); + } + + xcb_connection_t* handle; + } m_connection; + xcb_screen_t* const m_screen; + const SAtoms m_atoms; + // only touched on the event thread + core::unordered_map m_windows; + const CWindowXcb* m_dispatchingWindow = nullptr; + + struct SRequestParams_NOOP + { + using retval_t = void; + }; + struct SRequestParams_CreateWindow + { + using retval_t = xcb_window_t; + const IWindow::SCreationParams* params; + }; + struct SRequestParams_RegisterWindow + { + using retval_t = void; + CWindowXcb* window; + bool map; + }; + struct SRequestParams_DestroyWindow + { + using retval_t = void; + xcb_window_t window; + }; + struct SRequest + { + std::variant< + SRequestParams_NOOP, + SRequestParams_CreateWindow, + SRequestParams_RegisterWindow, + SRequestParams_DestroyWindow + > params = SRequestParams_NOOP(); + }; + static inline constexpr uint32_t CircularBufferSize = 256u; + class CAsyncQueue final : public system::IAsyncQueueDispatcher + { + using base_t = system::IAsyncQueueDispatcher; + + public: + // unlike Win32 we have members the thread reads, so only start it once they're initialized + inline CAsyncQueue(CWindowManagerXcb* manager) : base_t(), m_manager(manager) + { + this->start(); + this->waitForInitComplete(); + } + inline ~CAsyncQueue() + { + this->shutdown(); + } + + inline void init() {m_threadID = std::this_thread::get_id();} + + // like the Win32 manager, keep spinning so `background_work` can poll the X connection + inline bool wakeupPredicate() const { return true; } + inline bool continuePredicate() const { return true; } + + void background_work(); + + void process_request(base_t::future_base_t* _future_base, SRequest& req); + + inline bool isEventThread() const {return std::this_thread::get_id()==m_threadID;} + + private: + CWindowManagerXcb* const m_manager; + std::thread::id m_threadID; + }; + // Declared last so the event thread starts after all the other members are initialized + CAsyncQueue m_eventThread; +}; + +} +#endif +#endif diff --git a/src/nbl/ui/CWindowXcb.cpp b/src/nbl/ui/CWindowXcb.cpp new file mode 100644 index 0000000000..307e568d1c --- /dev/null +++ b/src/nbl/ui/CWindowXcb.cpp @@ -0,0 +1,170 @@ +#include "nbl/ui/CWindowXcb.h" +#include "nbl/ui/CWindowManagerXcb.h" + +#ifdef _NBL_PLATFORM_LINUX_ +#include + +using namespace nbl; +using namespace nbl::ui; + +CWindowXcb::CWindowXcb(SCreationParams&& params, core::smart_refctd_ptr&& winManager, xcb_window_t window) : + IWindowXcb(std::move(params)), m_windowManager(std::move(winManager)), m_native{m_windowManager->getConnection(),window} +{ + // not mapped yet, `onMapped` will clear it with an `onWindowShown` like `WM_SHOWWINDOW` does on Win32 + m_flags |= ECF_HIDDEN; + // the window manager tells us about these through `_NET_WM_STATE` once mapped + m_flags &= ~core::bitflag(ECF_MINIMIZED); + m_flags &= ~core::bitflag(ECF_MAXIMIZED); + // we track these ourselves from focus and crossing events + m_flags &= ~core::bitflag(ECF_INPUT_FOCUS); + m_flags &= ~core::bitflag(ECF_MOUSE_FOCUS); +} + +void CWindowXcb::setCaption(const std::string_view& caption) +{ + auto* const connection = m_native.connection; + const auto& atoms = m_windowManager->getAtoms(); + xcb_change_property(connection,XCB_PROP_MODE_REPLACE,m_native.window,XCB_ATOM_WM_NAME,XCB_ATOM_STRING,8,caption.size(),caption.data()); + xcb_change_property(connection,XCB_PROP_MODE_REPLACE,m_native.window,atoms._NET_WM_NAME,atoms.UTF8_STRING,8,caption.size(),caption.data()); + xcb_flush(connection); +} + +void CWindowXcb::onDeleteRequested() +{ + if (!m_cb || m_cb->onWindowClosed(this)) + { + // Don't destroy, a Vulkan surface or swapchain may still reference the X window. It goes away with the last `IWindow` reference. + xcb_unmap_window(m_native.connection,m_native.window); + xcb_flush(m_native.connection); + } +} + +void CWindowXcb::onConfigured(const xcb_configure_notify_event_t* ev) +{ + int32_t x = ev->x; + int32_t y = ev->y; + // Real events are relative to the parent, which is the frame of a reparenting window manager. + // Synthetic ones sent by the window manager are already in root coordinates (ICCCM 4.1.5). + if (!(ev->response_type&0x80u)) + { + auto* const connection = m_native.connection; + const auto cookie = xcb_translate_coordinates(connection,m_native.window,m_windowManager->getRootWindow(),0,0); + if (auto* reply=xcb_translate_coordinates_reply(connection,cookie,nullptr)) + { + x = reply->dst_x; + y = reply->dst_y; + free(reply); + } + } + if (!m_cb) + return; + if (x!=m_x || y!=m_y) + (void)m_cb->onWindowMoved(this,x,y); + if (ev->width!=m_width || ev->height!=m_height) + (void)m_cb->onWindowResized(this,ev->width,ev->height); +} + +void CWindowXcb::onMapped() +{ + // restoring from minimized maps the window again, `updateState` handles that + updateState(); + if (m_cb && isHidden()) + (void)m_cb->onWindowShown(this); +} + +void CWindowXcb::onUnmapped() +{ + // minimizing also unmaps, but that is not hiding the window + updateState(); + if (m_cb && !m_minimizedState && !isHidden()) + (void)m_cb->onWindowHidden(this); +} + +void CWindowXcb::onFocusChanged(const bool keyboard, const bool gained) +{ + const auto flag = keyboard ? ECF_INPUT_FOCUS:ECF_MOUSE_FOCUS; + // X sends several focus events per change (e.g. for the frame), only report real transitions + if (m_flags.hasFlags(flag)==gained) + return; + if (gained) + m_flags |= flag; + else + m_flags &= ~core::bitflag(flag); + if (!m_cb) + return; + if (keyboard) + { + if (gained) + m_cb->onGainedKeyboardFocus(this); + else + m_cb->onLostKeyboardFocus(this); + } + else + { + if (gained) + m_cb->onGainedMouseFocus(this); + else + m_cb->onLostMouseFocus(this); + } +} + +void CWindowXcb::updateState() +{ + auto* const connection = m_native.connection; + const auto& atoms = m_windowManager->getAtoms(); + + // send both requests before waiting + const auto netStateCookie = xcb_get_property(connection,0,m_native.window,atoms._NET_WM_STATE,XCB_ATOM_ATOM,0,32); + const auto wmStateCookie = xcb_get_property(connection,0,m_native.window,atoms.WM_STATE,atoms.WM_STATE,0,2); + + bool hidden = false, maximizedVert = false, maximizedHorz = false; + if (auto* reply=xcb_get_property_reply(connection,netStateCookie,nullptr)) + { + if (reply->format==32) + { + const auto* state = reinterpret_cast(xcb_get_property_value(reply)); + const auto count = xcb_get_property_value_length(reply)/sizeof(xcb_atom_t); + for (auto i=0u; i