diff --git a/3rdparty/CMakeLists.txt b/3rdparty/CMakeLists.txt index 6e7e9e9970..6d3e5dc4f9 100755 --- a/3rdparty/CMakeLists.txt +++ b/3rdparty/CMakeLists.txt @@ -66,10 +66,17 @@ 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) 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 +126,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,13 +155,28 @@ 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}") -add_custom_command(OUTPUT ${JPEG_LIB_OUTPUT} +# 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() +# always step into the child build (a no-op when it's up to date), its own dependency tracking then picks up +# source and flag changes (e.g. PIC) which a custom command OUTPUT without DEPENDS never would +add_custom_target(jpeg-static-build COMMAND "${CMAKE_COMMAND}" --build "${THIRD_PARTY_BINARY_DIR}/libjpeg-turbo" -t jpeg-static --config $ + BYPRODUCTS "${JPEG_LIB_OUTPUT}" COMMENT "Building libjpeg-turbo static library" VERBATIM USES_TERMINAL ) -add_library(jpeg-static INTERFACE ${JPEG_LIB_OUTPUT}) +add_library(jpeg-static INTERFACE) +add_dependencies(jpeg-static jpeg-static-build) target_include_directories(jpeg-static INTERFACE "${THIRD_PARTY_SOURCE_DIR}/libjpeg-turbo/src" "${THIRD_PARTY_BINARY_DIR}/libjpeg-turbo" diff --git a/3rdparty/dxc/CMakeLists.txt b/3rdparty/dxc/CMakeLists.txt index 506dd9cc60..cbacbf690d 100644 --- a/3rdparty/dxc/CMakeLists.txt +++ b/3rdparty/dxc/CMakeLists.txt @@ -102,6 +102,60 @@ 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) + # injected as a compile definition after DXC's project() so CMAKE_CXX_FLAGS/CXXFLAGS keep their usual meaning + set(_NBL_DXC_PROJECT_INCLUDE_ "${CMAKE_CURRENT_BINARY_DIR}/nbl-dxc-project-include.cmake") + file(CONFIGURE OUTPUT "${_NBL_DXC_PROJECT_INCLUDE_}" CONTENT "add_compile_definitions(CoGetMalloc=DxcCoGetMalloc)\n") + list(APPEND NBL_DXC_CMAKE_OPTIONS "-DCMAKE_PROJECT_INCLUDE:FILEPATH=${_NBL_DXC_PROJECT_INCLUDE_}") + + # 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..7c59ea48b5 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 @@ -195,6 +202,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() @@ -297,11 +311,20 @@ function(nbl_install_program_spec _TRGT _RELATIVE_DESTINATION) set(_DEST_GE_ "${_NBL_CPACK_PACKAGE_RELATIVE_ENTRY_}/runtime/${_RELATIVE_DESTINATION}") if (TARGET ${_TRGT}) + get_target_property(_TYPE_ ${_TRGT} TYPE) foreach(_CONFIGURATION_ IN LISTS CMAKE_CONFIGURATION_TYPES) - install(PROGRAMS $ DESTINATION ${_DEST_GE_} CONFIGURATIONS ${_CONFIGURATION_} COMPONENT Runtimes) + # a plain file copy keeps the build RPATH (absolute build tree paths) of an ELF/Mach-O shared object, + # install(TARGETS) swaps it for INSTALL_RPATH + if(NOT WIN32 AND _TYPE_ MATCHES "^(SHARED|MODULE)_LIBRARY$") + install(TARGETS ${_TRGT} LIBRARY DESTINATION ${_DEST_GE_} CONFIGURATIONS ${_CONFIGURATION_} COMPONENT Runtimes) + else() + install(PROGRAMS $ DESTINATION ${_DEST_GE_} CONFIGURATIONS ${_CONFIGURATION_} COMPONENT Runtimes) + endif() 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 +385,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/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..b598c22d4e --- /dev/null +++ b/docs/linux-build.md @@ -0,0 +1,199 @@ +# 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 + +`.gitmodules` points every submodule at an absolute URL. Upstream uses relative URLs for `3rdparty/boost/superproject` and `docker/msvc-winsdk`, and in a fork those resolve under the fork owner (`raydelto`), where they don't exist; `linux-port` uses the absolute `Devsh-Graphics-Programming` URLs instead (NAB-10). `Ditt-Reference-Scenes` is a private reference repository that must be excluded. + +Exclude the private scenes, pick the protocol (SSH or HTTPS), and initialize: + +```bash +# In your clone / worktree of raydelto/Nabla: +git checkout linux-port + +# Clones made before NAB-10 still carry the old relative URLs in .git/config; refresh them once: +git submodule sync -- 3rdparty/boost/superproject docker/msvc-winsdk + +# 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). diff --git a/examples_tests b/examples_tests index 4a0cc2f683..f6d661623b 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 4a0cc2f68349ac188c5a9208ae1cadb2684b4040 +Subproject commit f6d661623b1f569e1671081edbf6b09bed7e7ae6 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/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/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/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/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/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 4a2bce9552..986ded2303 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/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/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/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/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/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/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/CMakeLists.txt b/src/nbl/CMakeLists.txt index 2abcb329f7..c638933fd9 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 ) @@ -634,7 +636,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 ) @@ -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 @@ -756,6 +766,30 @@ 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. Ask the standard library itself +# which backend it picked (libc++ and older libstdc++ never use TBB, so a stray TBB header doesn't matter there). +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("#include \n#ifndef _PSTL_PAR_BACKEND_TBB\n#error no TBB backend\n#endif\nint main() {return 0;}" NBL_STDLIB_USES_TBB_PAR_BACKEND) + endblock() + if(NBL_STDLIB_USES_TBB_PAR_BACKEND) + find_package(TBB CONFIG QUIET) + if(TBB_FOUND) + target_link_libraries(Nabla PUBLIC TBB::tbb) + else() + # headers without the CMake package (or the library): fall back to the serial backend instead of failing to link + message(WARNING "libstdc++ found and would run std::execution policies on oneTBB, but TBBConfig.cmake was not found. " + "Building with _GLIBCXX_USE_TBB_PAR_BACKEND=0 (parallel algorithms run serially). Install the TBB development package " + "(Ubuntu: libtbb-dev) or point TBB_DIR at TBBConfig.cmake to use oneTBB." + ) + target_compile_definitions(Nabla PUBLIC _GLIBCXX_USE_TBB_PAR_BACKEND=0) + endif() + endif() +endif() + # vulkaninfo if (NBL_STATIC_BUILD) target_link_libraries(Nabla PUBLIC $) @@ -905,6 +939,12 @@ 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), +# (the build tree one finds it through the linker-generated build RPATH, install replaces that with INSTALL_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 INSTALL_RPATH "$ORIGIN/${_NBL_DXC_DIR_REL_TO_NABLA_}") +endif() set(_NBL_CONSUMER_BIN_DIR_GE_ "$,$>>" ) @@ -960,3 +1000,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/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/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/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..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) { @@ -390,8 +391,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 +406,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/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)) { 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