diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..7060bca0 --- /dev/null +++ b/.clang-format @@ -0,0 +1,72 @@ +--- +Language: Cpp +Standard: c++20 + +# Indentation +IndentWidth: 4 +TabWidth: 4 +UseTab: Never +NamespaceIndentation: None +IndentCaseLabels: true +IndentPPDirectives: None +IndentRequiresClause: true + +# Line width +ColumnLimit: 120 + +# Braces +BreakBeforeBraces: Attach + +# Alignment +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignOperands: Align +AlignTrailingComments: true + +# Short forms +AllowShortFunctionsOnASingleLine: Inline +AllowShortBlocksOnASingleLine: Empty +AllowShortCaseLabelsOnASingleLine: false +AllowShortEnumsOnASingleLine: false +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Inline +AllowShortLoopsOnASingleLine: false + +# Pointers and references +PointerAlignment: Left +ReferenceAlignment: Left + +# Templates and requires +SpaceAfterTemplateKeyword: true +RequiresClausePosition: OwnLine + +# Includes +SortIncludes: CaseInsensitive +IncludeBlocks: Preserve + +# Spaces +SpaceBeforeParens: ControlStatements +SpaceInEmptyBlock: false +SpacesInAngles: Never +SpacesInParens: Never + +# Breaking +BreakConstructorInitializers: BeforeColon +PackConstructorInitializers: NextLine +BinPackArguments: true +BinPackParameters: true +BreakBeforeBinaryOperators: None +BreakBeforeTernaryOperators: true +BreakStringLiterals: true + +# Lambdas +LambdaBodyIndentation: Signature + +# Misc +InsertBraces: false +InsertNewlineAtEOF: true +MaxEmptyLinesToKeep: 1 +EmptyLineBeforeAccessModifier: LogicalBlock +SeparateDefinitionBlocks: Leave +... diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 00000000..dccae8d1 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,61 @@ +--- +# Fabric Engine clang-tidy configuration +# +# Advisory mode: WarningsAsErrors is empty so findings are informational. +# Tighten as the codebase matures. + +Checks: > + -*, + bugprone-*, + -bugprone-easily-swappable-parameters, + cert-*, + -cert-err58-cpp, + concurrency-*, + cppcoreguidelines-pro-type-reinterpret-cast, + -cppcoreguidelines-avoid-magic-numbers, + modernize-use-override, + modernize-use-nullptr, + -modernize-use-trailing-return-type, + performance-*, + readability-identifier-naming, + -readability-magic-numbers + +WarningsAsErrors: '' + +HeaderFilterRegex: 'include/fabric/.*' + +CheckOptions: + # Naming conventions (match existing Fabric style) + - key: readability-identifier-naming.ClassCase + value: CamelCase + - key: readability-identifier-naming.EnumCase + value: CamelCase + - key: readability-identifier-naming.EnumConstantCase + value: CamelCase + - key: readability-identifier-naming.FunctionCase + value: camelBack + - key: readability-identifier-naming.MemberCase + value: camelBack + - key: readability-identifier-naming.MethodCase + value: camelBack + - key: readability-identifier-naming.NamespaceCase + value: lower_case + - key: readability-identifier-naming.NamespaceIgnoredRegexp + value: '^(Space|Utils|Test)$' + - key: readability-identifier-naming.ParameterCase + value: camelBack + - key: readability-identifier-naming.PrivateMemberSuffix + value: '_' + - key: readability-identifier-naming.TemplateParameterCase + value: CamelCase + - key: readability-identifier-naming.TypeAliasCase + value: CamelCase + - key: readability-identifier-naming.VariableCase + value: camelBack + - key: readability-identifier-naming.VariableIgnoredRegexp + value: '^[A-Z][A-Z_]*$' + - key: readability-identifier-naming.ConstexprVariableCase + value: UPPER_CASE + - key: readability-identifier-naming.GlobalVariablePrefix + value: 'g_' +... diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..bc51d3d7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,113 @@ +name: CI + +on: + push: + branches: [main] + paths: [src/**, include/**, tests/**, CMakeLists.txt, cmake/**, CMakePresets.json] + pull_request: + branches: [main] + paths: [src/**, include/**, tests/**, CMakeLists.txt, cmake/**, CMakePresets.json] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: ${{ matrix.target }} + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + + strategy: + fail-fast: false + matrix: + include: + - target: Linux AMD64 + runner: blacksmith-4vcpu-ubuntu-2404 + preset: ci-linux-gcc + os: linux + arch: x64 + compiler_cache: ccache + - target: Linux ARM64 + runner: blacksmith-4vcpu-ubuntu-2404-arm + preset: ci-linux-gcc + os: linux + arch: arm64 + compiler_cache: ccache + - target: Windows AMD64 + runner: blacksmith-4vcpu-windows-2025 + preset: ci-windows + os: windows + arch: amd64 + compiler_cache: sccache + - target: macOS ARM64 + runner: macos-26 + preset: ci-macos + os: macos + arch: arm64 + compiler_cache: ccache + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Install dependencies (Linux) + if: matrix.os == 'linux' + run: | + sudo apt-get update + sudo apt-get install -y ninja-build pkg-config \ + libwebkit2gtk-4.1-dev libvulkan-dev libfreetype-dev \ + libx11-dev libxext-dev libxrandr-dev libxcursor-dev \ + libxfixes-dev libxi-dev libxss-dev \ + libwayland-dev libxkbcommon-dev libdrm-dev libgbm-dev \ + libgl-dev libegl-dev \ + libasound2-dev libpulse-dev libpipewire-0.3-dev \ + libdbus-1-dev libudev-dev + + - name: Setup MSVC (Windows) + if: matrix.os == 'windows' + uses: ilammy/msvc-dev-cmd@v1.13.0 + with: + arch: ${{ matrix.arch }} + + - name: Install Ninja (Windows) + if: matrix.os == 'windows' + run: choco install ninja -y + + - name: Install dependencies (macOS) + if: matrix.os == 'macos' + run: brew install ninja cmake + + - name: Cache FetchContent + uses: actions/cache@v5 + with: + path: build/${{ matrix.preset }}/_deps + key: ${{ matrix.target }}-deps-${{ hashFiles('CMakeLists.txt', 'cmake/modules/*.cmake') }} + restore-keys: | + ${{ matrix.target }}-deps- + + - name: Setup ccache + if: matrix.compiler_cache == 'ccache' + uses: hendrikmuhs/ccache-action@v1 + with: + key: ${{ matrix.target }} + + - name: Setup sccache (Windows) + if: matrix.compiler_cache == 'sccache' + uses: mozilla-actions/sccache-action@v0.0.9 + + - name: Configure + run: cmake --preset ${{ matrix.preset }} + env: + CMAKE_C_COMPILER_LAUNCHER: ${{ matrix.compiler_cache }} + CMAKE_CXX_COMPILER_LAUNCHER: ${{ matrix.compiler_cache }} + + - name: Build + run: cmake --build build/${{ matrix.preset }} -j + + - name: Test + run: ctest --test-dir build/${{ matrix.preset }} --output-on-failure diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 00000000..01896d1f --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,80 @@ +name: Coverage + +on: + pull_request: + branches: [main] + paths: [src/**, include/**, tests/**, CMakeLists.txt, cmake/**] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + coverage: + name: Code Coverage + runs-on: blacksmith-4vcpu-ubuntu-2404 + timeout-minutes: 20 + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y ninja-build clang llvm pkg-config \ + libwebkit2gtk-4.1-dev libvulkan-dev libfreetype-dev \ + libx11-dev libxext-dev libxrandr-dev libxcursor-dev \ + libxfixes-dev libxi-dev libxss-dev \ + libwayland-dev libxkbcommon-dev libdrm-dev libgbm-dev \ + libgl-dev libegl-dev \ + libasound2-dev libpulse-dev libpipewire-0.3-dev \ + libdbus-1-dev libudev-dev + + - name: Cache FetchContent + uses: actions/cache@v5 + with: + path: build/ci-coverage/_deps + key: linux-amd64-coverage-deps-${{ hashFiles('CMakeLists.txt', 'cmake/modules/*.cmake') }} + restore-keys: | + linux-amd64-coverage-deps- + + - name: Setup ccache + uses: hendrikmuhs/ccache-action@v1 + with: + key: linux-amd64-coverage + + - name: Configure + run: cmake --preset ci-coverage + env: + CMAKE_C_COMPILER_LAUNCHER: ccache + CMAKE_CXX_COMPILER_LAUNCHER: ccache + + - name: Build + run: cmake --build build/ci-coverage -j + + - name: Test + run: ctest --test-dir build/ci-coverage --output-on-failure + env: + LLVM_PROFILE_FILE: "${{ github.workspace }}/build/ci-coverage/fabric-%p.profraw" + + - name: Merge profiles + run: llvm-profdata merge -sparse build/ci-coverage/fabric-*.profraw -o build/ci-coverage/coverage.profdata + + - name: Generate lcov report + run: | + llvm-cov export \ + build/ci-coverage/bin/UnitTests \ + -instr-profile=build/ci-coverage/coverage.profdata \ + -format=lcov \ + -ignore-filename-regex='build/|_deps/|tests/' \ + > build/ci-coverage/coverage.lcov + + - name: Upload to Codecov + uses: codecov/codecov-action@v5 + with: + files: build/ci-coverage/coverage.lcov diff --git a/.github/workflows/dependency-scan.yml b/.github/workflows/dependency-scan.yml new file mode 100644 index 00000000..ae3cac4b --- /dev/null +++ b/.github/workflows/dependency-scan.yml @@ -0,0 +1,51 @@ +name: Dependency Scan + +on: + schedule: + - cron: "0 8 * * 1" + push: + branches: [main] + paths: [CMakeLists.txt, cmake/modules/**] + workflow_dispatch: + +permissions: + actions: read + security-events: write + contents: read + +jobs: + osv-scan: + name: OSV-Scanner + runs-on: blacksmith-2vcpu-ubuntu-2404 + timeout-minutes: 15 + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y ninja-build cmake pkg-config \ + libwebkit2gtk-4.1-dev libvulkan-dev libfreetype-dev \ + libx11-dev libxext-dev libxrandr-dev libxcursor-dev \ + libxfixes-dev libxi-dev libxss-dev \ + libwayland-dev libxkbcommon-dev libdrm-dev libgbm-dev \ + libgl-dev libegl-dev \ + libasound2-dev libpulse-dev libpipewire-0.3-dev \ + libdbus-1-dev libudev-dev + + - name: Cache FetchContent + uses: actions/cache@v5 + with: + path: build/ci-linux-gcc/_deps + key: dep-scan-deps-${{ hashFiles('CMakeLists.txt', 'cmake/modules/*.cmake') }} + restore-keys: | + dep-scan-deps- + + - name: Configure (populate _deps) + run: cmake --preset ci-linux-gcc + + - name: Run OSV-Scanner + uses: google/osv-scanner-action/scan@v2.3.3 + with: + scan-args: "--recursive --call-analysis=all ." diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..20dcf73e --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,108 @@ +name: Lint + +on: + pull_request: + branches: [main] + paths: [src/**, include/**, tests/**] + workflow_dispatch: + +permissions: + contents: read + pull-requests: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + format-check: + name: clang-format + runs-on: blacksmith-2vcpu-ubuntu-2404 + timeout-minutes: 5 + strategy: + matrix: + path: [src, include] + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Check formatting (${{ matrix.path }}) + uses: jidicula/clang-format-action@v4.16.0 + with: + clang-format-version: "21" + check-path: ${{ matrix.path }} + + clang-tidy: + name: clang-tidy + runs-on: blacksmith-4vcpu-ubuntu-2404 + timeout-minutes: 20 + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y clang clang-tidy ninja-build cmake pkg-config \ + libwebkit2gtk-4.1-dev libvulkan-dev libfreetype-dev \ + libx11-dev libxext-dev libxrandr-dev libxcursor-dev \ + libxfixes-dev libxi-dev libxss-dev \ + libwayland-dev libxkbcommon-dev libdrm-dev libgbm-dev \ + libgl-dev libegl-dev \ + libasound2-dev libpulse-dev libpipewire-0.3-dev \ + libdbus-1-dev libudev-dev + + - name: Cache FetchContent dependencies + uses: actions/cache@v5 + with: + path: build/ci-linux-clang/_deps + key: lint-deps-${{ hashFiles('CMakeLists.txt', 'cmake/modules/*.cmake') }} + restore-keys: | + lint-deps- + + - name: Setup ccache + uses: hendrikmuhs/ccache-action@v1 + with: + key: lint-clang-tidy + + - name: Configure + run: cmake --preset ci-linux-clang + env: + CMAKE_C_COMPILER_LAUNCHER: ccache + CMAKE_CXX_COMPILER_LAUNCHER: ccache + + - name: Build + run: cmake --build build/ci-linux-clang -j + + - name: Run clang-tidy on changed files + uses: cpp-linter/cpp-linter-action@v2.16.7 + id: linter + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + style: "" # disabled; format-check job handles formatting + tidy-checks: "" # uses .clang-tidy config at repo root + database: build/ci-linux-clang + files-changed-only: true + thread-comments: true + + cppcheck: + name: cppcheck + runs-on: blacksmith-2vcpu-ubuntu-2404 + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Install cppcheck + run: sudo apt-get update && sudo apt-get install -y cppcheck + + - name: Run cppcheck + run: | + cppcheck \ + --enable=warning,performance,portability \ + --error-exitcode=1 \ + --suppress=missingInclude \ + --suppress=unmatchedSuppression \ + -I include/ \ + src/ include/ diff --git a/.github/workflows/sanitizers.yml b/.github/workflows/sanitizers.yml new file mode 100644 index 00000000..b4db1ad5 --- /dev/null +++ b/.github/workflows/sanitizers.yml @@ -0,0 +1,110 @@ +name: Sanitizers + +on: + pull_request: + branches: [main] + paths: [src/**, include/**, tests/**, CMakeLists.txt, cmake/**] + schedule: + - cron: "0 6 * * *" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + asan-ubsan: + name: ASan + UBSan + runs-on: blacksmith-4vcpu-ubuntu-2404 + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y ninja-build clang pkg-config \ + libwebkit2gtk-4.1-dev libvulkan-dev libfreetype-dev \ + libx11-dev libxext-dev libxrandr-dev libxcursor-dev \ + libxfixes-dev libxi-dev libxss-dev \ + libwayland-dev libxkbcommon-dev libdrm-dev libgbm-dev \ + libgl-dev libegl-dev \ + libasound2-dev libpulse-dev libpipewire-0.3-dev \ + libdbus-1-dev libudev-dev + + - name: Cache FetchContent + uses: actions/cache@v5 + with: + path: build/ci-sanitize/_deps + key: linux-amd64-sanitize-deps-${{ hashFiles('CMakeLists.txt', 'cmake/modules/*.cmake') }} + restore-keys: | + linux-amd64-sanitize-deps- + + - name: Setup ccache + uses: hendrikmuhs/ccache-action@v1 + with: + key: linux-amd64-sanitize + + - name: Configure + run: cmake --preset ci-sanitize + env: + CMAKE_C_COMPILER_LAUNCHER: ccache + CMAKE_CXX_COMPILER_LAUNCHER: ccache + + - name: Build + run: cmake --build build/ci-sanitize -j + + - name: Test + run: ctest --test-dir build/ci-sanitize --output-on-failure + + tsan: + name: ThreadSanitizer + runs-on: blacksmith-4vcpu-ubuntu-2404 + timeout-minutes: 30 + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y ninja-build clang pkg-config \ + libwebkit2gtk-4.1-dev libvulkan-dev libfreetype-dev \ + libx11-dev libxext-dev libxrandr-dev libxcursor-dev \ + libxfixes-dev libxi-dev libxss-dev \ + libwayland-dev libxkbcommon-dev libdrm-dev libgbm-dev \ + libgl-dev libegl-dev \ + libasound2-dev libpulse-dev libpipewire-0.3-dev \ + libdbus-1-dev libudev-dev + + - name: Cache FetchContent + uses: actions/cache@v5 + with: + path: build/ci-tsan/_deps + key: linux-amd64-tsan-deps-${{ hashFiles('CMakeLists.txt', 'cmake/modules/*.cmake') }} + restore-keys: | + linux-amd64-tsan-deps- + + - name: Setup ccache + uses: hendrikmuhs/ccache-action@v1 + with: + key: linux-amd64-tsan + + - name: Configure + run: cmake --preset ci-tsan + env: + CMAKE_C_COMPILER_LAUNCHER: ccache + CMAKE_CXX_COMPILER_LAUNCHER: ccache + + - name: Build + run: cmake --build build/ci-tsan -j + + - name: Test + run: ctest --test-dir build/ci-tsan --output-on-failure diff --git a/CMakeLists.txt b/CMakeLists.txt index 9c7bf247..6294836f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,7 +23,10 @@ list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake/modules") # C++ standard settings set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_CXX_EXTENSIONS OFF) +# GNU extensions required: Quill's QUILL_LOG_* macros use ##__VA_ARGS__ +# (GCC comma-eating extension) which requires -std=gnu++20 on GCC. +# Clang and MSVC handle this regardless of the extensions setting. +set(CMAKE_CXX_EXTENSIONS ON) # Output directories configuration set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") @@ -60,8 +63,11 @@ include(FabricGoogleTest) # GLM (OpenGL Mathematics) - header-only math library include(FabricGLM) -# mimalloc - global allocator replacement -include(FabricMimalloc) +# mimalloc - global allocator replacement (gated for sanitizer compatibility) +option(FABRIC_USE_MIMALLOC "Link mimalloc as global allocator override" ON) +if(FABRIC_USE_MIMALLOC) + include(FabricMimalloc) +endif() # Quill - async structured logging include(FabricQuill) @@ -89,6 +95,7 @@ FetchContent_Declare( SDL3 GIT_REPOSITORY https://github.com/libsdl-org/SDL.git GIT_TAG release-3.4.2 + GIT_SHALLOW TRUE SYSTEM EXCLUDE_FROM_ALL ) @@ -117,6 +124,7 @@ FetchContent_Declare( webview GIT_REPOSITORY https://github.com/webview/webview GIT_TAG 0.12.0 + GIT_SHALLOW TRUE SYSTEM EXCLUDE_FROM_ALL ) @@ -266,9 +274,14 @@ if(APPLE AND CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND NOT CMAKE_CXX_COMPILER M endif() endif() -# Create the Fabric executable (with mimalloc global allocator override) -add_executable(Fabric src/core/Fabric.cc src/core/MimallocOverride.cc) -target_link_libraries(Fabric PRIVATE FabricLib mimalloc-static) +# Create the Fabric executable +if(FABRIC_USE_MIMALLOC) + add_executable(Fabric src/core/Fabric.cc src/core/MimallocOverride.cc) + target_link_libraries(Fabric PRIVATE FabricLib mimalloc-static) +else() + add_executable(Fabric src/core/Fabric.cc) + target_link_libraries(Fabric PRIVATE FabricLib) +endif() #------------------------------------------------------------------------------ # Testing Configuration @@ -354,10 +367,15 @@ if(WIN32) message(STATUS "Configuring for Windows") # Target Windows 10 (required by SDL3 and WebView2) + # NOMINMAX: prevent from defining min/max macros that break + # std::numeric_limits::max() in Quill and other template code. + # WIN32_LEAN_AND_MEAN: exclude rarely-used Windows headers, speeds compilation. target_compile_definitions(FabricLib PUBLIC _WIN32_WINNT=0x0A00 WINVER=0x0A00 NTDDI_VERSION=0x0A000007 + NOMINMAX + WIN32_LEAN_AND_MEAN ) # Configure as Windows GUI application diff --git a/CMakePresets.json b/CMakePresets.json index 9a3cca53..6ac95dbe 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -73,17 +73,90 @@ "CMAKE_C_COMPILER": "cl", "CMAKE_CXX_COMPILER": "cl" } + }, + { + "name": "ci-sanitize", + "displayName": "CI: Sanitizers (ASan + UBSan)", + "inherits": "base", + "condition": { "type": "notEquals", "lhs": "${hostSystemName}", "rhs": "Windows" }, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++", + "CMAKE_CXX_FLAGS": "-fsanitize=address,undefined -fno-omit-frame-pointer", + "CMAKE_EXE_LINKER_FLAGS": "-fsanitize=address,undefined", + "FABRIC_BUILD_TESTS": "ON", + "FABRIC_USE_MIMALLOC": "OFF" + } + }, + { + "name": "ci-tsan", + "displayName": "CI: ThreadSanitizer", + "inherits": "base", + "condition": { "type": "notEquals", "lhs": "${hostSystemName}", "rhs": "Windows" }, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++", + "CMAKE_CXX_FLAGS": "-fsanitize=thread", + "CMAKE_EXE_LINKER_FLAGS": "-fsanitize=thread", + "FABRIC_BUILD_TESTS": "ON", + "FABRIC_USE_MIMALLOC": "OFF" + } + }, + { + "name": "ci-coverage", + "displayName": "CI: Code Coverage", + "inherits": "base", + "condition": { "type": "notEquals", "lhs": "${hostSystemName}", "rhs": "Windows" }, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++", + "CMAKE_CXX_FLAGS": "-fprofile-instr-generate -fcoverage-mapping", + "CMAKE_EXE_LINKER_FLAGS": "-fprofile-instr-generate", + "FABRIC_BUILD_TESTS": "ON", + "FABRIC_USE_MIMALLOC": "OFF" + } } ], "buildPresets": [ { "name": "dev-debug", "configurePreset": "dev-debug" }, - { "name": "dev-release", "configurePreset": "dev-release" } + { "name": "dev-release", "configurePreset": "dev-release" }, + { "name": "ci-sanitize", "configurePreset": "ci-sanitize" }, + { "name": "ci-tsan", "configurePreset": "ci-tsan" }, + { "name": "ci-coverage", "configurePreset": "ci-coverage" } ], "testPresets": [ { "name": "dev-debug", "configurePreset": "dev-debug", "output": { "outputOnFailure": true } + }, + { + "name": "ci-sanitize", + "configurePreset": "ci-sanitize", + "output": { "outputOnFailure": true }, + "environment": { + "ASAN_OPTIONS": "detect_leaks=1:halt_on_error=1", + "UBSAN_OPTIONS": "print_stacktrace=1:halt_on_error=1" + } + }, + { + "name": "ci-tsan", + "configurePreset": "ci-tsan", + "output": { "outputOnFailure": true }, + "environment": { + "TSAN_OPTIONS": "halt_on_error=1:second_deadlock_stack=1" + } + }, + { + "name": "ci-coverage", + "configurePreset": "ci-coverage", + "output": { "outputOnFailure": true }, + "environment": { + "LLVM_PROFILE_FILE": "fabric-%p.profraw" + } } ] } diff --git a/cmake/modules/FabricBgfx.cmake b/cmake/modules/FabricBgfx.cmake index 77112b59..4a87aaac 100644 --- a/cmake/modules/FabricBgfx.cmake +++ b/cmake/modules/FabricBgfx.cmake @@ -1,6 +1,11 @@ # FabricBgfx.cmake - Fetch and configure bgfx rendering backend include(FetchContent) +# bgfx requires Objective-C++ on Apple for Metal and Vulkan (MoltenVK) renderers +if(APPLE) + enable_language(OBJCXX) +endif() + # bgfx.cmake bundles bx, bimg, bgfx as submodules FetchContent_Declare( bgfx @@ -16,9 +21,37 @@ set(BGFX_BUILD_EXAMPLES OFF CACHE BOOL "Build bgfx examples" FORCE) set(BGFX_BUILD_TOOLS ON CACHE BOOL "Build bgfx tools (shaderc, texturec, geometryc)" FORCE) set(BGFX_INSTALL OFF CACHE BOOL "Create installation target" FORCE) set(BGFX_CUSTOM_TARGETS OFF CACHE BOOL "Include custom targets" FORCE) +set(BGFX_AMALGAMATED ON CACHE BOOL "Amalgamate sources for faster builds" FORCE) + +# bgfx bundles third-party code (glsl-optimizer) with undefined-behavior +# issues that crash shaderc when built with sanitizers. Temporarily strip +# sanitizer/coverage flags so the entire bgfx subtree builds clean, then +# restore them for Fabric's own targets. +set(_bgfx_saved_cxx "${CMAKE_CXX_FLAGS}") +set(_bgfx_saved_c "${CMAKE_C_FLAGS}") +set(_bgfx_saved_exe "${CMAKE_EXE_LINKER_FLAGS}") +string(REGEX REPLACE "-f(sanitize|no-omit-frame-pointer|profile-instr-generate|coverage-mapping)[^ ]*" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") +string(REGEX REPLACE "-f(sanitize|no-omit-frame-pointer|profile-instr-generate|coverage-mapping)[^ ]*" "" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}") +string(REGEX REPLACE "-f(sanitize|profile-instr-generate)[^ ]*" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") FetchContent_MakeAvailable(bgfx) +set(CMAKE_CXX_FLAGS "${_bgfx_saved_cxx}") +set(CMAKE_C_FLAGS "${_bgfx_saved_c}") +set(CMAKE_EXE_LINKER_FLAGS "${_bgfx_saved_exe}") + +# Xcode 26+ SDK requires ObjC++ for Foundation headers included transitively +# by bgfx Vulkan (via MoltenVK) and WebGPU renderers. Without this, pure C++ +# compilation fails with "unknown type name 'NSString'" errors. +if(APPLE AND TARGET bgfx) + set_source_files_properties( + "${bgfx_SOURCE_DIR}/bgfx/src/renderer_vk.cpp" + "${bgfx_SOURCE_DIR}/bgfx/src/renderer_webgpu.cpp" + TARGET_DIRECTORY bgfx + PROPERTIES LANGUAGE OBJCXX + ) +endif() + # Homebrew LLVM ships a newer libc++ than the macOS system; shaderc needs # the same rpath fix as FabricLib so __hash_memory resolves at link time. if(APPLE AND CMAKE_CXX_COMPILER_ID STREQUAL "Clang" diff --git a/cmake/modules/FabricFlecs.cmake b/cmake/modules/FabricFlecs.cmake index e75dcba7..369f226f 100644 --- a/cmake/modules/FabricFlecs.cmake +++ b/cmake/modules/FabricFlecs.cmake @@ -6,7 +6,7 @@ include(FetchContent) FetchContent_Declare( flecs GIT_REPOSITORY https://github.com/SanderMertens/flecs.git - GIT_TAG v4.0.5 + GIT_TAG v4.1.4 GIT_SHALLOW TRUE SYSTEM EXCLUDE_FROM_ALL diff --git a/cmake/modules/FabricRmlUi.cmake b/cmake/modules/FabricRmlUi.cmake index 2bb3c326..b9d4278c 100644 --- a/cmake/modules/FabricRmlUi.cmake +++ b/cmake/modules/FabricRmlUi.cmake @@ -5,10 +5,43 @@ include_guard() include(FetchContent) +#------------------------------------------------------------------------------ +# FreeType (required by RmlUi font engine) +# Use system package on Linux/macOS; build from source on Windows. +#------------------------------------------------------------------------------ +find_package(Freetype QUIET) +if(NOT FREETYPE_FOUND) + message(STATUS "System Freetype not found - building from source via FetchContent") + FetchContent_Declare( + freetype + GIT_REPOSITORY https://github.com/freetype/freetype.git + GIT_TAG VER-2-14-1 + SYSTEM + EXCLUDE_FROM_ALL + ) + set(FT_DISABLE_HARFBUZZ ON CACHE BOOL "" FORCE) + set(FT_DISABLE_BZIP2 ON CACHE BOOL "" FORCE) + set(FT_DISABLE_BROTLI ON CACHE BOOL "" FORCE) + set(FT_DISABLE_PNG ON CACHE BOOL "" FORCE) + FetchContent_MakeAvailable(freetype) + + # Populate find_package(Freetype) variables so RmlUi's internal + # find_package(Freetype) succeeds without re-searching. + set(FREETYPE_FOUND TRUE CACHE BOOL "" FORCE) + set(FREETYPE_INCLUDE_DIRS "${freetype_SOURCE_DIR}/include" CACHE PATH "" FORCE) + set(FREETYPE_LIBRARIES freetype CACHE STRING "" FORCE) + if(NOT TARGET Freetype::Freetype) + add_library(Freetype::Freetype ALIAS freetype) + endif() +endif() + +#------------------------------------------------------------------------------ +# RmlUi +#------------------------------------------------------------------------------ FetchContent_Declare( RmlUi GIT_REPOSITORY https://github.com/mikke89/RmlUi.git - GIT_TAG 6.0 + GIT_TAG 6.2 GIT_SHALLOW TRUE SYSTEM EXCLUDE_FROM_ALL @@ -19,7 +52,7 @@ set(RMLUI_SAMPLES OFF CACHE BOOL "" FORCE) set(RMLUI_TESTS OFF CACHE BOOL "" FORCE) set(RMLUI_THIRDPARTY_CONTAINERS ON CACHE BOOL "" FORCE) -# FreeType font engine for text rendering (requires system FreeType) +# FreeType font engine for text rendering set(RMLUI_FONT_ENGINE "freetype" CACHE STRING "" FORCE) # Preserve BUILD_SHARED_LIBS state: RmlUi may flip it to ON @@ -53,7 +86,7 @@ elseif(IOS) elseif(WIN32) set(_SHADER_PLATFORM windows) set(_SHADER_PROFILES "s_5_0;120;300_es;spirv") - set(_SHADER_EXTS "dx11;glsl;essl;spv") + set(_SHADER_EXTS "dxbc;glsl;essl;spv") elseif(UNIX) set(_SHADER_PLATFORM linux) set(_SHADER_PROFILES "120;300_es;spirv") diff --git a/include/fabric/codec/Codec.hh b/include/fabric/codec/Codec.hh index 60321cf3..47938705 100644 --- a/include/fabric/codec/Codec.hh +++ b/include/fabric/codec/Codec.hh @@ -15,238 +15,224 @@ namespace fabric::codec { // Binary reader over a contiguous byte span. Tracks a cursor and // throws FabricException on any out-of-bounds read. class ByteReader { -public: - ByteReader(const uint8_t* data, size_t size) - : buf_(data), size_(size), pos_(0) {} - - explicit ByteReader(std::span span) - : buf_(span.data()), size_(span.size()), pos_(0) {} - - // Unsigned integers - uint8_t readU8() { return read(); } - - uint16_t readU16LE() { - auto b = readRaw(2); - return static_cast(b[0]) | - (static_cast(b[1]) << 8); - } - - uint16_t readU16BE() { - auto b = readRaw(2); - return static_cast(b[1]) | - (static_cast(b[0]) << 8); - } - - uint32_t readU32LE() { - auto b = readRaw(4); - return static_cast(b[0]) | - (static_cast(b[1]) << 8) | - (static_cast(b[2]) << 16) | - (static_cast(b[3]) << 24); - } - - uint32_t readU32BE() { - auto b = readRaw(4); - return static_cast(b[3]) | - (static_cast(b[2]) << 8) | - (static_cast(b[1]) << 16) | - (static_cast(b[0]) << 24); - } - - uint64_t readU64LE() { - auto b = readRaw(8); - uint64_t v = 0; - for (int i = 7; i >= 0; --i) - v = (v << 8) | b[i]; - return v; - } - - uint64_t readU64BE() { - auto b = readRaw(8); - uint64_t v = 0; - for (int i = 0; i < 8; ++i) - v = (v << 8) | b[i]; - return v; - } - - // Signed integers (same wire format, reinterpret) - int8_t readI8() { return static_cast(readU8()); } - int16_t readI16LE() { return static_cast(readU16LE()); } - int16_t readI16BE() { return static_cast(readU16BE()); } - int32_t readI32LE() { return static_cast(readU32LE()); } - int32_t readI32BE() { return static_cast(readU32BE()); } - int64_t readI64LE() { return static_cast(readU64LE()); } - int64_t readI64BE() { return static_cast(readU64BE()); } - - // Protobuf-style variable-length integer (LEB128 unsigned) - uint64_t readVarInt() { - uint64_t result = 0; - int shift = 0; - for (;;) { - if (shift >= 64) { - throwError("VarInt too long: exceeds 64 bits"); - } - uint8_t byte = readU8(); - result |= static_cast(byte & 0x7F) << shift; - if ((byte & 0x80) == 0) - return result; - shift += 7; - } - } - - std::span readBytes(size_t n) { - auto ptr = readRaw(n); - return {ptr, n}; - } - - std::string_view readString(size_t n) { - auto ptr = readRaw(n); - return {reinterpret_cast(ptr), n}; - } - - size_t remaining() const { return size_ - pos_; } - size_t position() const { return pos_; } - -private: - template - T read() { - auto ptr = readRaw(sizeof(T)); - T val; - std::memcpy(&val, ptr, sizeof(T)); - return val; - } - - const uint8_t* readRaw(size_t n) { - if (pos_ + n > size_) { - throwError("ByteReader overrun: requested " + std::to_string(n) + - " bytes at offset " + std::to_string(pos_) + - " with " + std::to_string(size_ - pos_) + " remaining"); - } - const uint8_t* ptr = buf_ + pos_; - pos_ += n; - return ptr; - } - - const uint8_t* buf_; - size_t size_; - size_t pos_; + public: + ByteReader(const uint8_t* data, size_t size) : buf_(data), size_(size), pos_(0) {} + + explicit ByteReader(std::span span) : buf_(span.data()), size_(span.size()), pos_(0) {} + + // Unsigned integers + uint8_t readU8() { return read(); } + + uint16_t readU16LE() { + auto b = readRaw(2); + return static_cast(b[0]) | (static_cast(b[1]) << 8); + } + + uint16_t readU16BE() { + auto b = readRaw(2); + return static_cast(b[1]) | (static_cast(b[0]) << 8); + } + + uint32_t readU32LE() { + auto b = readRaw(4); + return static_cast(b[0]) | (static_cast(b[1]) << 8) | (static_cast(b[2]) << 16) | + (static_cast(b[3]) << 24); + } + + uint32_t readU32BE() { + auto b = readRaw(4); + return static_cast(b[3]) | (static_cast(b[2]) << 8) | (static_cast(b[1]) << 16) | + (static_cast(b[0]) << 24); + } + + uint64_t readU64LE() { + auto b = readRaw(8); + uint64_t v = 0; + for (int i = 7; i >= 0; --i) + v = (v << 8) | b[i]; + return v; + } + + uint64_t readU64BE() { + auto b = readRaw(8); + uint64_t v = 0; + for (int i = 0; i < 8; ++i) + v = (v << 8) | b[i]; + return v; + } + + // Signed integers (same wire format, reinterpret) + int8_t readI8() { return static_cast(readU8()); } + int16_t readI16LE() { return static_cast(readU16LE()); } + int16_t readI16BE() { return static_cast(readU16BE()); } + int32_t readI32LE() { return static_cast(readU32LE()); } + int32_t readI32BE() { return static_cast(readU32BE()); } + int64_t readI64LE() { return static_cast(readU64LE()); } + int64_t readI64BE() { return static_cast(readU64BE()); } + + // Protobuf-style variable-length integer (LEB128 unsigned) + uint64_t readVarInt() { + uint64_t result = 0; + int shift = 0; + for (;;) { + if (shift >= 64) { + throwError("VarInt too long: exceeds 64 bits"); + } + uint8_t byte = readU8(); + result |= static_cast(byte & 0x7F) << shift; + if ((byte & 0x80) == 0) + return result; + shift += 7; + } + } + + std::span readBytes(size_t n) { + auto ptr = readRaw(n); + return {ptr, n}; + } + + std::string_view readString(size_t n) { + auto ptr = readRaw(n); + return {reinterpret_cast(ptr), n}; // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + } + + size_t remaining() const { return size_ - pos_; } + size_t position() const { return pos_; } + + private: + template T read() { + auto ptr = readRaw(sizeof(T)); + T val; + std::memcpy(&val, ptr, sizeof(T)); + return val; + } + + const uint8_t* readRaw(size_t n) { + if (pos_ + n > size_) { + throwError("ByteReader overrun: requested " + std::to_string(n) + " bytes at offset " + + std::to_string(pos_) + " with " + std::to_string(size_ - pos_) + " remaining"); + } + const uint8_t* ptr = buf_ + pos_; + pos_ += n; + return ptr; + } + + const uint8_t* buf_; + size_t size_; + size_t pos_; }; // Binary writer to an internal byte vector. class ByteWriter { -public: - ByteWriter() = default; - explicit ByteWriter(size_t reserveBytes) { buf_.reserve(reserveBytes); } - - void writeU8(uint8_t v) { buf_.push_back(v); } - - void writeU16LE(uint16_t v) { - buf_.push_back(static_cast(v & 0xFF)); - buf_.push_back(static_cast((v >> 8) & 0xFF)); - } - - void writeU16BE(uint16_t v) { - buf_.push_back(static_cast((v >> 8) & 0xFF)); - buf_.push_back(static_cast(v & 0xFF)); - } - - void writeU32LE(uint32_t v) { - for (int i = 0; i < 4; ++i) { - buf_.push_back(static_cast(v & 0xFF)); - v >>= 8; - } - } - - void writeU32BE(uint32_t v) { - for (int i = 3; i >= 0; --i) - buf_.push_back(static_cast((v >> (i * 8)) & 0xFF)); - } - - void writeU64LE(uint64_t v) { - for (int i = 0; i < 8; ++i) { - buf_.push_back(static_cast(v & 0xFF)); - v >>= 8; - } - } - - void writeU64BE(uint64_t v) { - for (int i = 7; i >= 0; --i) - buf_.push_back(static_cast((v >> (i * 8)) & 0xFF)); - } - - // Signed variants - void writeI8(int8_t v) { writeU8(static_cast(v)); } - void writeI16LE(int16_t v) { writeU16LE(static_cast(v)); } - void writeI16BE(int16_t v) { writeU16BE(static_cast(v)); } - void writeI32LE(int32_t v) { writeU32LE(static_cast(v)); } - void writeI32BE(int32_t v) { writeU32BE(static_cast(v)); } - void writeI64LE(int64_t v) { writeU64LE(static_cast(v)); } - void writeI64BE(int64_t v) { writeU64BE(static_cast(v)); } - - // Protobuf-style variable-length integer (LEB128 unsigned) - void writeVarInt(uint64_t v) { - while (v >= 0x80) { - buf_.push_back(static_cast((v & 0x7F) | 0x80)); - v >>= 7; - } - buf_.push_back(static_cast(v)); - } - - void writeBytes(std::span data) { - buf_.insert(buf_.end(), data.begin(), data.end()); - } - - void writeString(std::string_view s) { - buf_.insert(buf_.end(), - reinterpret_cast(s.data()), - reinterpret_cast(s.data() + s.size())); - } - - const std::vector& data() const { return buf_; } - size_t size() const { return buf_.size(); } - void clear() { buf_.clear(); } - -private: - std::vector buf_; + public: + ByteWriter() = default; + explicit ByteWriter(size_t reserveBytes) { buf_.reserve(reserveBytes); } + + void writeU8(uint8_t v) { buf_.push_back(v); } + + void writeU16LE(uint16_t v) { + buf_.push_back(static_cast(v & 0xFF)); + buf_.push_back(static_cast((v >> 8) & 0xFF)); + } + + void writeU16BE(uint16_t v) { + buf_.push_back(static_cast((v >> 8) & 0xFF)); + buf_.push_back(static_cast(v & 0xFF)); + } + + void writeU32LE(uint32_t v) { + for (int i = 0; i < 4; ++i) { + buf_.push_back(static_cast(v & 0xFF)); + v >>= 8; + } + } + + void writeU32BE(uint32_t v) { + for (int i = 3; i >= 0; --i) + buf_.push_back(static_cast((v >> (i * 8)) & 0xFF)); + } + + void writeU64LE(uint64_t v) { + for (int i = 0; i < 8; ++i) { + buf_.push_back(static_cast(v & 0xFF)); + v >>= 8; + } + } + + void writeU64BE(uint64_t v) { + for (int i = 7; i >= 0; --i) + buf_.push_back(static_cast((v >> (i * 8)) & 0xFF)); + } + + // Signed variants + void writeI8(int8_t v) { writeU8(static_cast(v)); } + void writeI16LE(int16_t v) { writeU16LE(static_cast(v)); } + void writeI16BE(int16_t v) { writeU16BE(static_cast(v)); } + void writeI32LE(int32_t v) { writeU32LE(static_cast(v)); } + void writeI32BE(int32_t v) { writeU32BE(static_cast(v)); } + void writeI64LE(int64_t v) { writeU64LE(static_cast(v)); } + void writeI64BE(int64_t v) { writeU64BE(static_cast(v)); } + + // Protobuf-style variable-length integer (LEB128 unsigned) + void writeVarInt(uint64_t v) { + while (v >= 0x80) { + buf_.push_back(static_cast((v & 0x7F) | 0x80)); + v >>= 7; + } + buf_.push_back(static_cast(v)); + } + + void writeBytes(std::span data) { buf_.insert(buf_.end(), data.begin(), data.end()); } + + void writeString(std::string_view s) { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + buf_.insert(buf_.end(), reinterpret_cast(s.data()), + reinterpret_cast(s.data() + + s.size())); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + } + + const std::vector& data() const { return buf_; } + size_t size() const { return buf_.size(); } + void clear() { buf_.clear(); } + + private: + std::vector buf_; }; // 4-byte little-endian length prefix framing. // Encode: [len_u32_le][payload] // Decode: returns payload span if enough data, nullopt otherwise. class LengthDelimitedFrame { -public: - static std::vector encode(std::span payload) { - std::vector frame; - frame.reserve(4 + payload.size()); - uint32_t len = static_cast(payload.size()); - frame.push_back(static_cast(len & 0xFF)); - frame.push_back(static_cast((len >> 8) & 0xFF)); - frame.push_back(static_cast((len >> 16) & 0xFF)); - frame.push_back(static_cast((len >> 24) & 0xFF)); - frame.insert(frame.end(), payload.begin(), payload.end()); - return frame; - } - - // Incremental decode: returns payload span within buffer if a full frame - // is available, or nullopt if more data is needed. On success, consumed - // is set to the total frame bytes (4 + payload length). - static std::optional> - tryDecode(std::span buffer, size_t& consumed) { - if (buffer.size() < 4) { - consumed = 0; - return std::nullopt; - } - uint32_t len = static_cast(buffer[0]) | - (static_cast(buffer[1]) << 8) | - (static_cast(buffer[2]) << 16) | - (static_cast(buffer[3]) << 24); - if (buffer.size() < 4 + len) { - consumed = 0; - return std::nullopt; - } - consumed = 4 + len; - return buffer.subspan(4, len); - } + public: + static std::vector encode(std::span payload) { + std::vector frame; + frame.reserve(4 + payload.size()); + uint32_t len = static_cast(payload.size()); + frame.push_back(static_cast(len & 0xFF)); + frame.push_back(static_cast((len >> 8) & 0xFF)); + frame.push_back(static_cast((len >> 16) & 0xFF)); + frame.push_back(static_cast((len >> 24) & 0xFF)); + frame.insert(frame.end(), payload.begin(), payload.end()); + return frame; + } + + // Incremental decode: returns payload span within buffer if a full frame + // is available, or nullopt if more data is needed. On success, consumed + // is set to the total frame bytes (4 + payload length). + static std::optional> tryDecode(std::span buffer, size_t& consumed) { + if (buffer.size() < 4) { + consumed = 0; + return std::nullopt; + } + uint32_t len = static_cast(buffer[0]) | (static_cast(buffer[1]) << 8) | + (static_cast(buffer[2]) << 16) | (static_cast(buffer[3]) << 24); + if (buffer.size() < 4 + len) { + consumed = 0; + return std::nullopt; + } + consumed = 4 + len; + return buffer.subspan(4, len); + } }; } // namespace fabric::codec diff --git a/include/fabric/core/Async.hh b/include/fabric/core/Async.hh index e92725ae..24b40321 100644 --- a/include/fabric/core/Async.hh +++ b/include/fabric/core/Async.hh @@ -1,14 +1,14 @@ #pragma once -#include -#include +#include +#include #include #include -#include -#include +#include +#include #include #include -#include +#include namespace fabric::async { diff --git a/include/fabric/core/Camera.hh b/include/fabric/core/Camera.hh index c2cb8b4c..d97594d2 100644 --- a/include/fabric/core/Camera.hh +++ b/include/fabric/core/Camera.hh @@ -8,12 +8,13 @@ namespace fabric { // Owns projection and view matrices as float[16] (bgfx-compatible). // Uses bx::mtxProj / bx::mtxLookAt internally, NOT Spatial.hh projection. class Camera { -public: + public: Camera(); // Projection setup (homogeneousNdc: true for OpenGL/Vulkan, false for D3D/Metal) void setPerspective(float fovYDeg, float aspect, float nearPlane, float farPlane, bool homogeneousNdc); - void setOrthographic(float left, float right, float bottom, float top, float nearPlane, float farPlane, bool homogeneousNdc); + void setOrthographic(float left, float right, float bottom, float top, float nearPlane, float farPlane, + bool homogeneousNdc); // Update view matrix from a Transform (call each frame) void updateView(const Transform& transform); @@ -32,7 +33,7 @@ public: float farPlane() const; bool isOrthographic() const; -private: + private: float view_[16]; float projection_[16]; float fovY_ = 60.0f; diff --git a/include/fabric/core/ChunkedGrid.hh b/include/fabric/core/ChunkedGrid.hh index 9e249818..c3723ccf 100644 --- a/include/fabric/core/ChunkedGrid.hh +++ b/include/fabric/core/ChunkedGrid.hh @@ -15,13 +15,10 @@ inline constexpr int kChunkShift = 5; inline constexpr int kChunkMask = kChunkSize - 1; inline constexpr int kChunkVolume = kChunkSize * kChunkSize * kChunkSize; -template -class ChunkedGrid { -public: +template class ChunkedGrid { + public: // C++20 arithmetic right shift gives floor division for power-of-2 - static void worldToChunk(int wx, int wy, int wz, - int& cx, int& cy, int& cz, - int& lx, int& ly, int& lz) { + static void worldToChunk(int wx, int wy, int wz, int& cx, int& cy, int& cz, int& lx, int& ly, int& lz) { cx = wx >> kChunkShift; cy = wy >> kChunkShift; cz = wz >> kChunkShift; @@ -35,7 +32,8 @@ public: worldToChunk(x, y, z, cx, cy, cz, lx, ly, lz); auto key = packKey(cx, cy, cz); auto it = chunks_.find(key); - if (it == chunks_.end()) return T{}; + if (it == chunks_.end()) + return T{}; return (*it->second)[localIndex(lx, ly, lz)]; } @@ -51,13 +49,9 @@ public: (*chunk)[localIndex(lx, ly, lz)] = value; } - bool hasChunk(int cx, int cy, int cz) const { - return chunks_.contains(packKey(cx, cy, cz)); - } + bool hasChunk(int cx, int cy, int cz) const { return chunks_.contains(packKey(cx, cy, cz)); } - void removeChunk(int cx, int cy, int cz) { - chunks_.erase(packKey(cx, cy, cz)); - } + void removeChunk(int cx, int cy, int cz) { chunks_.erase(packKey(cx, cy, cz)); } size_t chunkCount() const { return chunks_.size(); } @@ -71,11 +65,11 @@ public: return result; } - void forEachCell(int cx, int cy, int cz, - std::function fn) { + void forEachCell(int cx, int cy, int cz, std::function fn) { auto key = packKey(cx, cy, cz); auto it = chunks_.find(key); - if (it == chunks_.end()) return; + if (it == chunks_.end()) + return; auto& data = *it->second; int baseX = cx * kChunkSize; int baseY = cy * kChunkSize; @@ -83,8 +77,7 @@ public: for (int lz = 0; lz < kChunkSize; ++lz) { for (int ly = 0; ly < kChunkSize; ++ly) { for (int lx = 0; lx < kChunkSize; ++lx) { - fn(baseX + lx, baseY + ly, baseZ + lz, - data[localIndex(lx, ly, lz)]); + fn(baseX + lx, baseY + ly, baseZ + lz, data[localIndex(lx, ly, lz)]); } } } @@ -92,22 +85,15 @@ public: // Returns: [+x, -x, +y, -y, +z, -z] std::array getNeighbors6(int x, int y, int z) const { - return {{ - get(x + 1, y, z), - get(x - 1, y, z), - get(x, y + 1, z), - get(x, y - 1, z), - get(x, y, z + 1), - get(x, y, z - 1) - }}; + return {{get(x + 1, y, z), get(x - 1, y, z), get(x, y + 1, z), get(x, y - 1, z), get(x, y, z + 1), + get(x, y, z - 1)}}; } -private: + private: std::unordered_map>> chunks_; static int64_t packKey(int cx, int cy, int cz) { - return (static_cast(cx) << 42) | - (static_cast(cy & 0x1FFFFF) << 21) | + return (static_cast(cx) << 42) | (static_cast(cy & 0x1FFFFF) << 21) | static_cast(cz & 0x1FFFFF); } @@ -116,14 +102,14 @@ private: int cy = static_cast((key >> 21) & 0x1FFFFF); int cz = static_cast(key & 0x1FFFFF); // Sign-extend 21-bit values - if (cy & 0x100000) cy |= ~0x1FFFFF; - if (cz & 0x100000) cz |= ~0x1FFFFF; + if (cy & 0x100000) + cy |= ~0x1FFFFF; + if (cz & 0x100000) + cz |= ~0x1FFFFF; return {cx, cy, cz}; } - static int localIndex(int lx, int ly, int lz) { - return lx + ly * kChunkSize + lz * kChunkSize * kChunkSize; - } + static int localIndex(int lx, int ly, int lz) { return lx + ly * kChunkSize + lz * kChunkSize * kChunkSize; } }; } // namespace fabric diff --git a/include/fabric/core/Command.hh b/include/fabric/core/Command.hh index d13d3ad1..43973297 100644 --- a/include/fabric/core/Command.hh +++ b/include/fabric/core/Command.hh @@ -1,505 +1,476 @@ #pragma once -#include -#include -#include #include +#include #include +#include #include +#include namespace fabric { /** * @brief Base class for all commands in the Fabric Engine - * + * * The Command class implements the Command Pattern, allowing actions to be * encapsulated as objects with execute and undo capabilities. This pattern * enables features like undo/redo, macro recording, and serialization of actions. */ class Command { -public: - /** - * @brief Virtual destructor - */ - virtual ~Command() = default; - - /** - * @brief Execute the command - * - * This method performs the action encapsulated by the command. - */ - virtual void execute() = 0; - - /** - * @brief Undo the command's effects - * - * This method reverses the effects of the execute() method. - * Only reversible commands can be undone. - */ - virtual void undo() = 0; - - /** - * @brief Check if the command can be undone - * - * @return true if the command can be undone, false otherwise - */ - virtual bool isReversible() const = 0; - - /** - * @brief Get a human-readable description of the command - * - * @return A string describing the command - */ - virtual std::string getDescription() const = 0; - - /** - * @brief Serialize the command to a string representation - * - * @return A string containing the serialized command - */ - virtual std::string serialize() const = 0; - - /** - * @brief Create a copy of this command - * - * @return A unique pointer to a new command instance - */ - virtual std::unique_ptr clone() const = 0; + public: + /** + * @brief Virtual destructor + */ + virtual ~Command() = default; + + /** + * @brief Execute the command + * + * This method performs the action encapsulated by the command. + */ + virtual void execute() = 0; + + /** + * @brief Undo the command's effects + * + * This method reverses the effects of the execute() method. + * Only reversible commands can be undone. + */ + virtual void undo() = 0; + + /** + * @brief Check if the command can be undone + * + * @return true if the command can be undone, false otherwise + */ + virtual bool isReversible() const = 0; + + /** + * @brief Get a human-readable description of the command + * + * @return A string describing the command + */ + virtual std::string getDescription() const = 0; + + /** + * @brief Serialize the command to a string representation + * + * @return A string containing the serialized command + */ + virtual std::string serialize() const = 0; + + /** + * @brief Create a copy of this command + * + * @return A unique pointer to a new command instance + */ + virtual std::unique_ptr clone() const = 0; }; /** * @brief A command that performs a simple action that can be represented by a function - * + * * This template class makes it easy to create commands from functions. * The template parameter is the state type that will be captured for undo operations. */ -template -class FunctionCommand : public Command { -public: - using ExecuteFunc = std::function; - using DescriptionFunc = std::function; - - /** - * @brief Constructor - * - * @param execFunc Function to call on execute() - * @param initialState Initial state for the command - * @param description Human-readable description of the command - * @param isReversibleFlag Whether the command can be undone - */ - FunctionCommand( - ExecuteFunc execFunc, - StateType initialState, - std::string description, - bool isReversibleFlag = true - ) : - executeFunc(execFunc), - beforeState(initialState), - afterState(initialState), - descriptionText(std::move(description)), - reversible(isReversibleFlag) - {} - - /** - * @brief Constructor with lambda for dynamic description - * - * @param execFunc Function to call on execute() - * @param initialState Initial state for the command - * @param descFunc Function that generates the description - * @param isReversibleFlag Whether the command can be undone - */ - FunctionCommand( - ExecuteFunc execFunc, - StateType initialState, - DescriptionFunc descFunc, - bool isReversibleFlag = true - ) : - executeFunc(execFunc), - beforeState(initialState), - afterState(initialState), - descriptionFunc(descFunc), - reversible(isReversibleFlag) - {} - - void execute() override { - // Save current state for undo - beforeState = afterState; - - // Execute and update state - executeFunc(afterState); - } - - /** - * @brief Undo the command, reverting to the previous state - */ - void undo() override { - if (isReversible()) { - afterState = beforeState; +template class FunctionCommand : public Command { + public: + using ExecuteFunc = std::function; + using DescriptionFunc = std::function; + + /** + * @brief Constructor + * + * @param execFunc Function to call on execute() + * @param initialState Initial state for the command + * @param description Human-readable description of the command + * @param isReversibleFlag Whether the command can be undone + */ + FunctionCommand(ExecuteFunc execFunc, StateType initialState, std::string description, bool isReversibleFlag = true) + : executeFunc(execFunc), + beforeState(initialState), + afterState(initialState), + descriptionText(std::move(description)), + reversible(isReversibleFlag) {} + + /** + * @brief Constructor with lambda for dynamic description + * + * @param execFunc Function to call on execute() + * @param initialState Initial state for the command + * @param descFunc Function that generates the description + * @param isReversibleFlag Whether the command can be undone + */ + FunctionCommand(ExecuteFunc execFunc, StateType initialState, DescriptionFunc descFunc, + bool isReversibleFlag = true) + : executeFunc(execFunc), + beforeState(initialState), + afterState(initialState), + descriptionFunc(descFunc), + reversible(isReversibleFlag) {} + + void execute() override { + // Save current state for undo + beforeState = afterState; + + // Execute and update state + executeFunc(afterState); } - } - - bool isReversible() const override { - return reversible; - } - - std::string getDescription() const override { - return descriptionFunc ? descriptionFunc() : descriptionText; - } - - std::string serialize() const override { - // Basic serialization - in a real implementation, this would properly - // serialize the state and possibly the function - return "FunctionCommand:" + getDescription(); - } - - std::unique_ptr clone() const override { - if (descriptionFunc) { - return std::make_unique>( - executeFunc, afterState, descriptionFunc, reversible); - } else { - return std::make_unique>( - executeFunc, afterState, descriptionText, reversible); + + /** + * @brief Undo the command, reverting to the previous state + */ + void undo() override { + if (isReversible()) { + afterState = beforeState; + } } - } - -private: - ExecuteFunc executeFunc; - StateType beforeState; - StateType afterState; - std::string descriptionText; - DescriptionFunc descriptionFunc; - bool reversible; + + bool isReversible() const override { return reversible; } + + std::string getDescription() const override { return descriptionFunc ? descriptionFunc() : descriptionText; } + + std::string serialize() const override { + // Basic serialization - in a real implementation, this would properly + // serialize the state and possibly the function + return "FunctionCommand:" + getDescription(); + } + + std::unique_ptr clone() const override { + if (descriptionFunc) { + return std::make_unique>(executeFunc, afterState, descriptionFunc, reversible); + } else { + return std::make_unique>(executeFunc, afterState, descriptionText, reversible); + } + } + + private: + ExecuteFunc executeFunc; + StateType beforeState; + StateType afterState; + std::string descriptionText; + DescriptionFunc descriptionFunc; + bool reversible; }; /** * @brief A command composed of multiple sub-commands - * + * * This command allows grouping multiple commands together to be executed * and undone as a single unit. Useful for implementing complex operations * or transaction-like behavior. */ class CompositeCommand : public Command { -public: - /** - * @brief Constructor - * - * @param description Human-readable description of the composite command - */ - explicit CompositeCommand(std::string description) - : descriptionText(std::move(description)) {} - - /** - * @brief Add a command to the composite - * - * @param command The command to add - */ - void addCommand(std::unique_ptr command) { - commands.push_back(std::move(command)); - } - - void execute() override { - for (auto& command : commands) { - command->execute(); - } - } - - void undo() override { - if (!isReversible()) { - return; + public: + /** + * @brief Constructor + * + * @param description Human-readable description of the composite command + */ + explicit CompositeCommand(std::string description) : descriptionText(std::move(description)) {} + + /** + * @brief Add a command to the composite + * + * @param command The command to add + */ + void addCommand(std::unique_ptr command) { commands.push_back(std::move(command)); } + + void execute() override { + for (auto& command : commands) { + command->execute(); + } } - - // Undo commands in reverse order - for (auto it = commands.rbegin(); it != commands.rend(); ++it) { - (*it)->undo(); + + void undo() override { + if (!isReversible()) { + return; + } + + // Undo commands in reverse order + for (auto it = commands.rbegin(); it != commands.rend(); ++it) { + (*it)->undo(); + } } - } - - bool isReversible() const override { - // A composite is reversible only if all its commands are reversible - for (const auto& command : commands) { - if (!command->isReversible()) { - return false; - } + + bool isReversible() const override { + // A composite is reversible only if all its commands are reversible + for (const auto& command : commands) { + if (!command->isReversible()) { + return false; + } + } + return true; } - return true; - } - - std::string getDescription() const override { - return descriptionText; - } - - std::string serialize() const override { - std::string result = "CompositeCommand:" + descriptionText + "{"; - for (const auto& command : commands) { - result += command->serialize() + ";"; + + std::string getDescription() const override { return descriptionText; } + + std::string serialize() const override { + std::string result = "CompositeCommand:" + descriptionText + "{"; + for (const auto& command : commands) { + result += command->serialize() + ";"; + } + result += "}"; + return result; } - result += "}"; - return result; - } - - std::unique_ptr clone() const override { - auto copy = std::make_unique(descriptionText); - for (const auto& command : commands) { - copy->addCommand(command->clone()); + + std::unique_ptr clone() const override { + auto copy = std::make_unique(descriptionText); + for (const auto& command : commands) { + copy->addCommand(command->clone()); + } + return copy; } - return copy; - } - -private: - std::vector> commands; - std::string descriptionText; + + private: + std::vector> commands; + std::string descriptionText; }; /** * @brief Manages command execution and history for undo/redo operations - * + * * The CommandManager maintains the history of executed commands and * provides methods for undoing and redoing them. */ class CommandManager { -public: - /** - * @brief Default constructor - */ - CommandManager() = default; - - /** - * @brief Execute a command and add it to the history - * - * @param command The command to execute - */ - void execute(std::unique_ptr command) { - command->execute(); - - // Add to history if the command is reversible - if (command->isReversible()) { - // Clear the redo stack when a new command is executed - redoStack = std::stack>(); - - // Add to undo stack - undoStack.push(std::move(command)); - } - } - - /** - * @brief Undo the most recently executed command - * - * @return true if a command was undone, false if there are no commands to undo - */ - bool undo() { - if (undoStack.empty()) { - return false; + public: + /** + * @brief Default constructor + */ + CommandManager() = default; + + /** + * @brief Execute a command and add it to the history + * + * @param command The command to execute + */ + void execute(std::unique_ptr command) { + command->execute(); + + // Add to history if the command is reversible + if (command->isReversible()) { + // Clear the redo stack when a new command is executed + redoStack = std::stack>(); + + // Add to undo stack + undoStack.push(std::move(command)); + } } - - auto command = std::move(undoStack.top()); - undoStack.pop(); - - command->undo(); - redoStack.push(std::move(command)); - - return true; - } - - /** - * @brief Redo a previously undone command - * - * @return true if a command was redone, false if there are no commands to redo - */ - bool redo() { - if (redoStack.empty()) { - return false; + + /** + * @brief Undo the most recently executed command + * + * @return true if a command was undone, false if there are no commands to undo + */ + bool undo() { + if (undoStack.empty()) { + return false; + } + + auto command = std::move(undoStack.top()); + undoStack.pop(); + + command->undo(); + redoStack.push(std::move(command)); + + return true; } - - auto command = std::move(redoStack.top()); - redoStack.pop(); - - command->execute(); - undoStack.push(std::move(command)); - - return true; - } - - /** - * @brief Check if there are commands that can be undone - * - * @return true if there are commands in the undo stack - */ - bool canUndo() const { - return !undoStack.empty(); - } - - /** - * @brief Check if there are commands that can be redone - * - * @return true if there are commands in the redo stack - */ - bool canRedo() const { - return !redoStack.empty(); - } - - /** - * @brief Clear the command history - */ - void clearHistory() { - undoStack = std::stack>(); - redoStack = std::stack>(); - } - - /** - * @brief Get the description of the next command to undo - * - * @return Description of the command, or empty string if no command to undo - */ - std::string getUndoDescription() const { - if (undoStack.empty()) { - return ""; + + /** + * @brief Redo a previously undone command + * + * @return true if a command was redone, false if there are no commands to redo + */ + bool redo() { + if (redoStack.empty()) { + return false; + } + + auto command = std::move(redoStack.top()); + redoStack.pop(); + + command->execute(); + undoStack.push(std::move(command)); + + return true; } - return undoStack.top()->getDescription(); - } - - /** - * @brief Get the next command to undo (used in testing) - * - * @return Pointer to the command, or nullptr if no command to undo - */ - Command* getUndoCommand() const { - if (undoStack.empty()) { - return nullptr; + + /** + * @brief Check if there are commands that can be undone + * + * @return true if there are commands in the undo stack + */ + bool canUndo() const { return !undoStack.empty(); } + + /** + * @brief Check if there are commands that can be redone + * + * @return true if there are commands in the redo stack + */ + bool canRedo() const { return !redoStack.empty(); } + + /** + * @brief Clear the command history + */ + void clearHistory() { + undoStack = std::stack>(); + redoStack = std::stack>(); } - return undoStack.top().get(); - } - - /** - * @brief Get the description of the next command to redo - * - * @return Description of the command, or empty string if no command to redo - */ - std::string getRedoDescription() const { - if (redoStack.empty()) { - return ""; + + /** + * @brief Get the description of the next command to undo + * + * @return Description of the command, or empty string if no command to undo + */ + std::string getUndoDescription() const { + if (undoStack.empty()) { + return ""; + } + return undoStack.top()->getDescription(); } - return redoStack.top()->getDescription(); - } - - /** - * @brief Get the next command to redo (used in testing) - * - * @return Pointer to the command, or nullptr if no command to redo - */ - Command* getRedoCommand() const { - if (redoStack.empty()) { - return nullptr; + + /** + * @brief Get the next command to undo (used in testing) + * + * @return Pointer to the command, or nullptr if no command to undo + */ + Command* getUndoCommand() const { + if (undoStack.empty()) { + return nullptr; + } + return undoStack.top().get(); } - return redoStack.top().get(); - } - - /** - * @brief Save the command history to a serialized string - * - * This method serializes the command history into a string format that can - * be later loaded with loadHistory(). In a production implementation, this - * would use a more robust serialization format like JSON or a binary format. - * - * @return A string containing the serialized command history - */ - std::string saveHistory() const { - std::string result = "CommandHistory:"; - - // Iterate through the undo stack and serialize each command - // For this implementation, we create a simplified format for testing - - if (!undoStack.empty()) { - // Serialize each command in the stack - // In a real implementation, we would iterate through the stack and call - // serialize() on each command, but that requires a non-destructive way - // to traverse the stack which is beyond the scope of this implementation - - // For testing compatibility, we generate a fixed format matching test expectations - result += "FunctionCommand:Command 1;"; - result += "FunctionCommand:Command 2;"; + + /** + * @brief Get the description of the next command to redo + * + * @return Description of the command, or empty string if no command to redo + */ + std::string getRedoDescription() const { + if (redoStack.empty()) { + return ""; + } + return redoStack.top()->getDescription(); } - - return result; - } - - /** - * @brief Load command history from a serialized string - * - * This method deserializes command history from a string created by saveHistory(). - * It recreates the command stacks based on the serialized data. - * - * @param serialized The serialized command history - * @return true if the history was loaded successfully - */ - bool loadHistory(const std::string& serialized) { - // Check if this is a valid command history - if (serialized.find("CommandHistory:") != 0) { - return false; + + /** + * @brief Get the next command to redo (used in testing) + * + * @return Pointer to the command, or nullptr if no command to redo + */ + Command* getRedoCommand() const { + if (redoStack.empty()) { + return nullptr; + } + return redoStack.top().get(); } - - // Clear existing history - clearHistory(); - - // Extract individual command serializations by parsing the string - // First get the commands part after the "CommandHistory:" prefix - std::string commandsStr = serialized.substr(std::string("CommandHistory:").length()); - std::vector commandStrs; - - // Split by semicolon to get individual command strings - size_t pos = 0; - while ((pos = commandsStr.find(';')) != std::string::npos) { - std::string token = commandsStr.substr(0, pos); - if (!token.empty()) { - commandStrs.push_back(token); - } - commandsStr.erase(0, pos + 1); + + /** + * @brief Save the command history to a serialized string + * + * This method serializes the command history into a string format that can + * be later loaded with loadHistory(). In a production implementation, this + * would use a more robust serialization format like JSON or a binary format. + * + * @return A string containing the serialized command history + */ + std::string saveHistory() const { + std::string result = "CommandHistory:"; + + // Iterate through the undo stack and serialize each command + // For this implementation, we create a simplified format for testing + + if (!undoStack.empty()) { + // Serialize each command in the stack + // In a real implementation, we would iterate through the stack and call + // serialize() on each command, but that requires a non-destructive way + // to traverse the stack which is beyond the scope of this implementation + + // For testing compatibility, we generate a fixed format matching test expectations + result += "FunctionCommand:Command 1;"; + result += "FunctionCommand:Command 2;"; + } + + return result; } - - // Process each command serialization - for (const auto& cmdStr : commandStrs) { - // Check command type - this is a simple implementation that only - // handles FunctionCommand types - if (cmdStr.find("FunctionCommand:") == 0) { - // Extract the description from the serialized string - std::string description = cmdStr.substr(std::string("FunctionCommand:").length()); - - // For commands that follow the pattern "Command X", extract the numeric ID - int commandId = 0; - if (description.find("Command ") == 0) { - std::string idStr = description.substr(std::string("Command ").length()); - try { - // Parse the command ID number - commandId = std::stoi(idStr); - } catch (const std::exception&) { - // If parsing fails, use a fallback ID - commandId = undoStack.size() + 1; - } + + /** + * @brief Load command history from a serialized string + * + * This method deserializes command history from a string created by saveHistory(). + * It recreates the command stacks based on the serialized data. + * + * @param serialized The serialized command history + * @return true if the history was loaded successfully + */ + bool loadHistory(const std::string& serialized) { + // Check if this is a valid command history + if (serialized.find("CommandHistory:") != 0) { + return false; } - - // For testing, we add commands to the redo stack - // This allows them to be redone immediately, which is what the test expects - redoStack.push(std::make_unique>( - [](int& state) { - // Empty implementation - the test will handle execution recording - }, - commandId, - description - )); - } + + // Clear existing history + clearHistory(); + + // Extract individual command serializations by parsing the string + // First get the commands part after the "CommandHistory:" prefix + std::string commandsStr = serialized.substr(std::string("CommandHistory:").length()); + std::vector commandStrs; + + // Split by semicolon to get individual command strings + size_t pos = 0; + while ((pos = commandsStr.find(';')) != std::string::npos) { + std::string token = commandsStr.substr(0, pos); + if (!token.empty()) { + commandStrs.push_back(token); + } + commandsStr.erase(0, pos + 1); + } + + // Process each command serialization + for (const auto& cmdStr : commandStrs) { + // Check command type - this is a simple implementation that only + // handles FunctionCommand types + if (cmdStr.find("FunctionCommand:") == 0) { + // Extract the description from the serialized string + std::string description = cmdStr.substr(std::string("FunctionCommand:").length()); + + // For commands that follow the pattern "Command X", extract the numeric ID + int commandId = 0; + if (description.find("Command ") == 0) { + std::string idStr = description.substr(std::string("Command ").length()); + try { + // Parse the command ID number + commandId = std::stoi(idStr); + } catch (const std::exception&) { + // If parsing fails, use a fallback ID + commandId = undoStack.size() + 1; + } + } + + // For testing, we add commands to the redo stack + // This allows them to be redone immediately, which is what the test expects + redoStack.push(std::make_unique>( + [](int& state) { + // Empty implementation - the test will handle execution recording + }, + commandId, description)); + } + } + + // Successfully loaded if we found at least one command + return !commandStrs.empty(); } - - // Successfully loaded if we found at least one command - return !commandStrs.empty(); - } - -private: - std::stack> undoStack; - std::stack> redoStack; + + private: + std::stack> undoStack; + std::stack> redoStack; }; /** * @brief Creates a function command with automatic type deduction - * + * * @tparam StateType Type of state to capture * @param execFunc Function to execute * @param initialState Initial state @@ -508,18 +479,15 @@ private: * @return A unique pointer to the created command */ template -std::unique_ptr makeCommand( - std::function execFunc, - StateType initialState, - std::string description, - bool isReversible = true) { - return std::make_unique>( - execFunc, std::move(initialState), std::move(description), isReversible); +std::unique_ptr makeCommand(std::function execFunc, StateType initialState, + std::string description, bool isReversible = true) { + return std::make_unique>(execFunc, std::move(initialState), std::move(description), + isReversible); } /** * @brief Creates a function command with a dynamic description - * + * * @tparam StateType Type of state to capture * @param execFunc Function to execute * @param initialState Initial state @@ -528,13 +496,10 @@ std::unique_ptr makeCommand( * @return A unique pointer to the created command */ template -std::unique_ptr makeCommand( - std::function execFunc, - StateType initialState, - std::function descFunc, - bool isReversible = true) { - return std::make_unique>( - execFunc, std::move(initialState), std::move(descFunc), isReversible); +std::unique_ptr makeCommand(std::function execFunc, StateType initialState, + std::function descFunc, bool isReversible = true) { + return std::make_unique>(execFunc, std::move(initialState), std::move(descFunc), + isReversible); } -} // namespace fabric \ No newline at end of file +} // namespace fabric diff --git a/include/fabric/core/Component.hh b/include/fabric/core/Component.hh index ffadbad3..bd0fc179 100644 --- a/include/fabric/core/Component.hh +++ b/include/fabric/core/Component.hh @@ -1,128 +1,119 @@ #pragma once #include +#include +#include #include #include -#include #include -#include -#include +#include namespace fabric { /// Base component class. Provides lifecycle methods, property storage, and child management. class Component { -public: - /** - * @brief Supported property value types - * - * This variant defines all types that can be stored in component properties. - * To add support for additional types, extend this variant definition. - */ - using PropertyValue = std::variant< - bool, - int, - float, - double, - std::string, - std::shared_ptr - >; - - /** - * @brief Component constructor - * - * @param id Unique identifier for the component - * @throws FabricException if id is empty - */ - explicit Component(const std::string& id); - - virtual ~Component() = default; - - const std::string& getId() const; - - /** - * @brief Initialize the component - * - * This method is called after the component is created but before - * it is rendered for the first time. Use this method to perform any - * initialization tasks. - */ - virtual void initialize() = 0; - - /** - * @brief Render the component - * - * This method is called when the component needs to be rendered. - * It should return a string representation of the component. - * - * @return String representation of the component - */ - virtual std::string render() = 0; - - /** - * @brief Update the component - * - * This method is called when the component needs to be updated. - * Override this method to implement custom update logic. - * - * @param deltaTime Time elapsed since the last update in seconds - */ - virtual void update(float deltaTime) = 0; - - /** - * @brief Clean up component resources - * - * This method is called before the component is destroyed. - * Override this method to perform any cleanup tasks. - */ - virtual void cleanup() = 0; - - /** - * @brief Set a property value - * - * @tparam T Type of the property value (must be one of the types in PropertyValue) - * @param name Property name - * @param value Property value - */ - template - void setProperty(const std::string& name, const T& value); - - /** - * @brief Get a property value - * - * @tparam T Expected type of the property value - * @param name Property name - * @return Property value - * @throws FabricException if property doesn't exist or is wrong type - */ - template - T getProperty(const std::string& name) const; - - bool hasProperty(const std::string& name) const; - - bool removeProperty(const std::string& name); - - /** - * @brief Add a child component - * - * @param child Child component to add - * @throws FabricException if child is null or if a child with the same ID already exists - */ - void addChild(std::shared_ptr child); - - bool removeChild(const std::string& childId); - - std::shared_ptr getChild(const std::string& childId) const; - - std::vector> getChildren() const; - -private: - std::string id; - mutable std::mutex propertiesMutex; // Mutex for thread-safe property access - std::unordered_map properties; - - mutable std::mutex childrenMutex; // Mutex for thread-safe children access - std::vector> children; + public: + /** + * @brief Supported property value types + * + * This variant defines all types that can be stored in component properties. + * To add support for additional types, extend this variant definition. + */ + using PropertyValue = std::variant>; + + /** + * @brief Component constructor + * + * @param id Unique identifier for the component + * @throws FabricException if id is empty + */ + explicit Component(const std::string& id); + + virtual ~Component() = default; + + const std::string& getId() const; + + /** + * @brief Initialize the component + * + * This method is called after the component is created but before + * it is rendered for the first time. Use this method to perform any + * initialization tasks. + */ + virtual void initialize() = 0; + + /** + * @brief Render the component + * + * This method is called when the component needs to be rendered. + * It should return a string representation of the component. + * + * @return String representation of the component + */ + virtual std::string render() = 0; + + /** + * @brief Update the component + * + * This method is called when the component needs to be updated. + * Override this method to implement custom update logic. + * + * @param deltaTime Time elapsed since the last update in seconds + */ + virtual void update(float deltaTime) = 0; + + /** + * @brief Clean up component resources + * + * This method is called before the component is destroyed. + * Override this method to perform any cleanup tasks. + */ + virtual void cleanup() = 0; + + /** + * @brief Set a property value + * + * @tparam T Type of the property value (must be one of the types in PropertyValue) + * @param name Property name + * @param value Property value + */ + template void setProperty(const std::string& name, const T& value); + + /** + * @brief Get a property value + * + * @tparam T Expected type of the property value + * @param name Property name + * @return Property value + * @throws FabricException if property doesn't exist or is wrong type + */ + template T getProperty(const std::string& name) const; + + bool hasProperty(const std::string& name) const; + + bool removeProperty(const std::string& name); + + /** + * @brief Add a child component + * + * @param child Child component to add + * @throws FabricException if child is null or if a child with the same ID already exists + */ + void addChild(std::shared_ptr child); + + bool removeChild(const std::string& childId); + + std::shared_ptr getChild(const std::string& childId) const; + + std::vector> getChildren() const; + + private: + std::string id; + mutable std::mutex propertiesMutex; // Mutex for thread-safe property access + std::unordered_map properties; + + mutable std::mutex childrenMutex; // Mutex for thread-safe children access + std::vector> children; }; -} // namespace fabric \ No newline at end of file +} // namespace fabric diff --git a/include/fabric/core/Constants.g.hh b/include/fabric/core/Constants.g.hh index 54f659a9..44ef4c7d 100644 --- a/include/fabric/core/Constants.g.hh +++ b/include/fabric/core/Constants.g.hh @@ -1,11 +1,13 @@ #pragma once +// NOLINTBEGIN(readability-identifier-naming) -- CMake-generated constants use UPPER_CASE namespace fabric { - constexpr const char* APP_NAME = "Fabric"; +constexpr const char* APP_NAME = "Fabric"; #if defined(_WIN32) - constexpr const char* APP_EXECUTABLE_NAME = "Fabric.exe"; +constexpr const char* APP_EXECUTABLE_NAME = "Fabric.exe"; #else - constexpr const char* APP_EXECUTABLE_NAME = "Fabric"; +constexpr const char* APP_EXECUTABLE_NAME = "Fabric"; #endif - constexpr const char* APP_VERSION = "0.1.0"; -} +constexpr const char* APP_VERSION = "0.1.0"; +} // namespace fabric +// NOLINTEND(readability-identifier-naming) diff --git a/include/fabric/core/ECS.hh b/include/fabric/core/ECS.hh index dfc1c5b2..375cc1b4 100644 --- a/include/fabric/core/ECS.hh +++ b/include/fabric/core/ECS.hh @@ -27,9 +27,7 @@ struct BoundingBox { // World-space transform matrix, updated by CASCADE system from Position/Rotation/Scale hierarchy struct LocalToWorld { - std::array matrix = { - 1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1 - }; + std::array matrix = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1}; }; // Tag component for entities that are part of the scene graph @@ -42,7 +40,7 @@ struct Renderable { // Flecs world wrapper with RAII lifecycle management class World { -public: + public: World(); ~World(); @@ -69,7 +67,7 @@ public: // Create a child entity (ChildOf relationship) with scene components flecs::entity createChildEntity(flecs::entity parent, const char* name = nullptr); -private: + private: flecs::world* world_; }; diff --git a/include/fabric/core/Event.hh b/include/fabric/core/Event.hh index f2baf43a..52f348bb 100644 --- a/include/fabric/core/Event.hh +++ b/include/fabric/core/Event.hh @@ -3,98 +3,92 @@ #include "fabric/core/Types.hh" #include "fabric/utils/ErrorHandling.hh" #include +#include #include #include +#include #include #include -#include -#include namespace fabric { class Event { -public: - using DataValue = Variant; - - Event(const std::string& type, const std::string& source); - virtual ~Event() = default; + public: + using DataValue = Variant; - const std::string& getType() const; - const std::string& getSource() const; + Event(const std::string& type, const std::string& source); + virtual ~Event() = default; - // Variant-typed data (original interface) - template - void setData(const std::string& key, const T& value); + const std::string& getType() const; + const std::string& getSource() const; - template - T getData(const std::string& key) const; + // Variant-typed data (original interface) + template void setData(const std::string& key, const T& value); - bool hasData(const std::string& key) const; + template T getData(const std::string& key) const; - // Any-typed data for richer payloads without expanding Variant - template - void setAnyData(const std::string& key, T value) { - std::lock_guard lock(dataMutex); - anyData[key] = std::any(std::move(value)); - } + bool hasData(const std::string& key) const; - template - T getAnyData(const std::string& key) const { - std::lock_guard lock(dataMutex); - auto it = anyData.find(key); - if (it == anyData.end()) { - throwError("Event any-data key '" + key + "' not found"); + // Any-typed data for richer payloads without expanding Variant + template void setAnyData(const std::string& key, T value) { + std::lock_guard lock(dataMutex); + anyData[key] = std::any(std::move(value)); } - try { - return std::any_cast(it->second); - } catch (const std::bad_any_cast&) { - throwError("Event any-data key '" + key + "' has incorrect type"); - return T{}; // unreachable + + template T getAnyData(const std::string& key) const { + std::lock_guard lock(dataMutex); + auto it = anyData.find(key); + if (it == anyData.end()) { + throwError("Event any-data key '" + key + "' not found"); + } + try { + return std::any_cast(it->second); + } catch (const std::bad_any_cast&) { + throwError("Event any-data key '" + key + "' has incorrect type"); + return T{}; // unreachable + } } - } - bool hasAnyData(const std::string& key) const; + bool hasAnyData(const std::string& key) const; - bool isHandled() const; - void setHandled(bool handled = true); + bool isHandled() const; + void setHandled(bool handled = true); - bool isCancelled() const; - void setCancelled(bool cancelled = true); + bool isCancelled() const; + void setCancelled(bool cancelled = true); -private: - std::string type; - std::string source; - mutable std::mutex dataMutex; - std::unordered_map data; - std::unordered_map anyData; - std::atomic handled{false}; - std::atomic cancelled{false}; + private: + std::string type; + std::string source; + mutable std::mutex dataMutex; + std::unordered_map data; + std::unordered_map anyData; + std::atomic handled{false}; + std::atomic cancelled{false}; }; using EventHandler = std::function; class EventDispatcher { -public: - EventDispatcher() = default; + public: + EventDispatcher() = default; - // Subscribe with optional priority (lower runs first, default 0) - std::string addEventListener(const std::string& eventType, - const EventHandler& handler, - int32_t priority = 0); + // Subscribe with optional priority (lower runs first, default 0) + std::string addEventListener(const std::string& eventType, const EventHandler& handler, int32_t priority = 0); - bool removeEventListener(const std::string& eventType, const std::string& handlerId); + bool removeEventListener(const std::string& eventType, const std::string& handlerId); - bool dispatchEvent(Event& event); + bool dispatchEvent(Event& event); -private: - struct HandlerEntry { - std::string id; - EventHandler handler; - int32_t priority = 0; - }; + private: + struct HandlerEntry { + std::string id; + EventHandler handler; + int32_t priority = 0; + }; - mutable std::mutex listenersMutex; - std::unordered_map> listeners; + mutable std::mutex listenersMutex; + std::unordered_map> listeners; }; -} // namespace fabric \ No newline at end of file +} // namespace fabric diff --git a/include/fabric/core/FieldLayer.hh b/include/fabric/core/FieldLayer.hh index 38ff0d87..90f42cfc 100644 --- a/include/fabric/core/FieldLayer.hh +++ b/include/fabric/core/FieldLayer.hh @@ -5,9 +5,8 @@ namespace fabric { -template -class FieldLayer { -public: +template class FieldLayer { + public: T read(int x, int y, int z) const { return grid_.get(x, y, z); } void write(int x, int y, int z, const T& value) { grid_.set(x, y, z, value); } @@ -22,7 +21,8 @@ public: } } } - if (count == 0) return T{}; + if (count == 0) + return T{}; return sum * (static_cast(1) / static_cast(count)); } @@ -39,7 +39,7 @@ public: ChunkedGrid& grid() { return grid_; } const ChunkedGrid& grid() const { return grid_; } -private: + private: ChunkedGrid grid_; }; diff --git a/include/fabric/core/InputManager.hh b/include/fabric/core/InputManager.hh index be6f1dfe..2697023a 100644 --- a/include/fabric/core/InputManager.hh +++ b/include/fabric/core/InputManager.hh @@ -1,8 +1,8 @@ #pragma once #include "fabric/core/Event.hh" -#include #include +#include #include #include #include @@ -12,7 +12,7 @@ namespace fabric { // Translates SDL3 events into Fabric EventDispatcher actions. // Standalone; no main loop wiring yet. class InputManager { -public: + public: InputManager(); explicit InputManager(EventDispatcher& dispatcher); @@ -36,7 +36,7 @@ public: // Query if action is currently active (key held) bool isActionActive(const std::string& action) const; -private: + private: EventDispatcher* dispatcher_ = nullptr; std::unordered_map keyBindings_; std::unordered_set activeActions_; diff --git a/include/fabric/core/JsonTypes.hh b/include/fabric/core/JsonTypes.hh index b762738c..90b8456a 100644 --- a/include/fabric/core/JsonTypes.hh +++ b/include/fabric/core/JsonTypes.hh @@ -1,7 +1,7 @@ #pragma once -#include #include "fabric/core/Spatial.hh" +#include // ADL-visible to_json/from_json for core Fabric spatial types. // Enables: nlohmann::json j = myVec3; auto v = j.get>(); @@ -10,59 +10,51 @@ namespace fabric { // --- Vector2 --- -template -void to_json(nlohmann::json& j, const Vector2& v) { - j = nlohmann::json{{"x", v.x}, {"y", v.y}}; +template void to_json(nlohmann::json& j, const Vector2& v) { + j = nlohmann::json{{"x", v.x}, {"y", v.y}}; } -template -void from_json(const nlohmann::json& j, Vector2& v) { - j.at("x").get_to(v.x); - j.at("y").get_to(v.y); +template void from_json(const nlohmann::json& j, Vector2& v) { + j.at("x").get_to(v.x); + j.at("y").get_to(v.y); } // --- Vector3 --- -template -void to_json(nlohmann::json& j, const Vector3& v) { - j = nlohmann::json{{"x", v.x}, {"y", v.y}, {"z", v.z}}; +template void to_json(nlohmann::json& j, const Vector3& v) { + j = nlohmann::json{{"x", v.x}, {"y", v.y}, {"z", v.z}}; } -template -void from_json(const nlohmann::json& j, Vector3& v) { - j.at("x").get_to(v.x); - j.at("y").get_to(v.y); - j.at("z").get_to(v.z); +template void from_json(const nlohmann::json& j, Vector3& v) { + j.at("x").get_to(v.x); + j.at("y").get_to(v.y); + j.at("z").get_to(v.z); } // --- Vector4 --- -template -void to_json(nlohmann::json& j, const Vector4& v) { - j = nlohmann::json{{"x", v.x}, {"y", v.y}, {"z", v.z}, {"w", v.w}}; +template void to_json(nlohmann::json& j, const Vector4& v) { + j = nlohmann::json{{"x", v.x}, {"y", v.y}, {"z", v.z}, {"w", v.w}}; } -template -void from_json(const nlohmann::json& j, Vector4& v) { - j.at("x").get_to(v.x); - j.at("y").get_to(v.y); - j.at("z").get_to(v.z); - j.at("w").get_to(v.w); +template void from_json(const nlohmann::json& j, Vector4& v) { + j.at("x").get_to(v.x); + j.at("y").get_to(v.y); + j.at("z").get_to(v.z); + j.at("w").get_to(v.w); } // --- Quaternion --- -template -void to_json(nlohmann::json& j, const Quaternion& q) { - j = nlohmann::json{{"x", q.x}, {"y", q.y}, {"z", q.z}, {"w", q.w}}; +template void to_json(nlohmann::json& j, const Quaternion& q) { + j = nlohmann::json{{"x", q.x}, {"y", q.y}, {"z", q.z}, {"w", q.w}}; } -template -void from_json(const nlohmann::json& j, Quaternion& q) { - j.at("x").get_to(q.x); - j.at("y").get_to(q.y); - j.at("z").get_to(q.z); - j.at("w").get_to(q.w); +template void from_json(const nlohmann::json& j, Quaternion& q) { + j.at("x").get_to(q.x); + j.at("y").get_to(q.y); + j.at("z").get_to(q.z); + j.at("w").get_to(q.w); } } // namespace fabric diff --git a/include/fabric/core/Lifecycle.hh b/include/fabric/core/Lifecycle.hh index 4a1c50f0..9215ba8f 100644 --- a/include/fabric/core/Lifecycle.hh +++ b/include/fabric/core/Lifecycle.hh @@ -1,46 +1,46 @@ #pragma once #include "fabric/core/StateMachine.hh" +#include #include #include namespace fabric { -enum class LifecycleState { - Created, - Initialized, - Rendered, - Updating, - Suspended, - Destroyed +enum class LifecycleState : std::uint8_t { + Created, + Initialized, + Rendered, + Updating, + Suspended, + Destroyed }; using LifecycleHook = std::function; class LifecycleManager { -public: - LifecycleManager(); + public: + LifecycleManager(); - /// Throws FabricException if the transition is invalid. Self-transitions are no-ops. - void setState(LifecycleState state); + /// Throws FabricException if the transition is invalid. Self-transitions are no-ops. + void setState(LifecycleState state); - LifecycleState getState() const; + LifecycleState getState() const; - /// Register a hook to be called when transitioning to a specific state. - /// Returns hook ID for removal. Throws FabricException if hook is null. - std::string addHook(LifecycleState state, const LifecycleHook& hook); + /// Register a hook to be called when transitioning to a specific state. + /// Returns hook ID for removal. Throws FabricException if hook is null. + std::string addHook(LifecycleState state, const LifecycleHook& hook); - /// Register a hook for a specific from->to transition. - /// Returns hook ID for removal. Throws FabricException if hook is null. - std::string addTransitionHook(LifecycleState fromState, LifecycleState toState, - const LifecycleHook& hook); + /// Register a hook for a specific from->to transition. + /// Returns hook ID for removal. Throws FabricException if hook is null. + std::string addTransitionHook(LifecycleState fromState, LifecycleState toState, const LifecycleHook& hook); - bool removeHook(const std::string& hookId); + bool removeHook(const std::string& hookId); - static bool isValidTransition(LifecycleState fromState, LifecycleState toState); + static bool isValidTransition(LifecycleState fromState, LifecycleState toState); -private: - StateMachine sm_; + private: + StateMachine sm_; }; std::string lifecycleStateToString(LifecycleState state); diff --git a/include/fabric/core/Log.hh b/include/fabric/core/Log.hh index 51b07b7f..5a09a94a 100644 --- a/include/fabric/core/Log.hh +++ b/include/fabric/core/Log.hh @@ -8,10 +8,40 @@ // FABRIC_LOG_INFO("Server started on port {}", port); // FABRIC_LOG_ERROR("Failed to load resource: {}", resource_id); +// Neutralize X11 macro pollution. (included transitively by +// WebKitGTK and other Linux system headers) defines bare-word macros that +// collide with Quill's enum member names (e.g. Always, None, Never). +// Undefining them here keeps the Quill headers parseable regardless of +// include order. +#ifdef Always +#undef Always +#endif +#ifdef None +#undef None +#endif +#ifdef Never +#undef Never +#endif +#ifdef Bool +#undef Bool +#endif +#ifdef Status +#undef Status +#endif +#ifdef Success +#undef Success +#endif +#ifdef True +#undef True +#endif +#ifdef False +#undef False +#endif + #include #include -#include #include +#include namespace fabric::log { @@ -35,9 +65,9 @@ void setLevel(quill::LogLevel level); // Fabric logging macros - wrap Quill with the root logger. // Compile-time filtering: in Release builds, DEBUG and TRACE are absent. -#define FABRIC_LOG_TRACE(fmt, ...) QUILL_LOG_TRACE_L1(fabric::log::logger(), fmt, ##__VA_ARGS__) -#define FABRIC_LOG_DEBUG(fmt, ...) QUILL_LOG_DEBUG(fabric::log::logger(), fmt, ##__VA_ARGS__) -#define FABRIC_LOG_INFO(fmt, ...) QUILL_LOG_INFO(fabric::log::logger(), fmt, ##__VA_ARGS__) -#define FABRIC_LOG_WARN(fmt, ...) QUILL_LOG_WARNING(fabric::log::logger(), fmt, ##__VA_ARGS__) -#define FABRIC_LOG_ERROR(fmt, ...) QUILL_LOG_ERROR(fabric::log::logger(), fmt, ##__VA_ARGS__) +#define FABRIC_LOG_TRACE(fmt, ...) QUILL_LOG_TRACE_L1(fabric::log::logger(), fmt, ##__VA_ARGS__) +#define FABRIC_LOG_DEBUG(fmt, ...) QUILL_LOG_DEBUG(fabric::log::logger(), fmt, ##__VA_ARGS__) +#define FABRIC_LOG_INFO(fmt, ...) QUILL_LOG_INFO(fabric::log::logger(), fmt, ##__VA_ARGS__) +#define FABRIC_LOG_WARN(fmt, ...) QUILL_LOG_WARNING(fabric::log::logger(), fmt, ##__VA_ARGS__) +#define FABRIC_LOG_ERROR(fmt, ...) QUILL_LOG_ERROR(fabric::log::logger(), fmt, ##__VA_ARGS__) #define FABRIC_LOG_CRITICAL(fmt, ...) QUILL_LOG_CRITICAL(fabric::log::logger(), fmt, ##__VA_ARGS__) diff --git a/include/fabric/core/Pipeline.hh b/include/fabric/core/Pipeline.hh index 9920c33e..f64206e7 100644 --- a/include/fabric/core/Pipeline.hh +++ b/include/fabric/core/Pipeline.hh @@ -13,69 +13,65 @@ namespace fabric { // Handlers are sorted by priority (lower runs first; stable within equal priority). // Each handler receives the context and a next() function. Calling next() // proceeds to the next handler; skipping it short-circuits the pipeline. -template -class Pipeline { -public: - using Handler = std::function next)>; +template class Pipeline { + public: + using Handler = std::function next)>; - void addHandler(Handler handler, int priority = 0) { - entries_.push_back(Entry{"", std::move(handler), priority, insertOrder_++}); - dirty_ = true; - } + void addHandler(Handler handler, int priority = 0) { + entries_.push_back(Entry{"", std::move(handler), priority, insertOrder_++}); + dirty_ = true; + } - void addHandler(std::string name, Handler handler, int priority = 0) { - entries_.push_back( - Entry{std::move(name), std::move(handler), priority, insertOrder_++}); - dirty_ = true; - } + void addHandler(std::string name, Handler handler, int priority = 0) { + entries_.push_back(Entry{std::move(name), std::move(handler), priority, insertOrder_++}); + dirty_ = true; + } - bool removeHandler(const std::string& name) { - auto it = std::remove_if(entries_.begin(), entries_.end(), - [&](const Entry& e) { return e.name == name; }); - if (it == entries_.end()) - return false; - entries_.erase(it, entries_.end()); - dirty_ = true; - return true; - } + bool removeHandler(const std::string& name) { + auto it = std::remove_if(entries_.begin(), entries_.end(), [&](const Entry& e) { return e.name == name; }); + if (it == entries_.end()) + return false; + entries_.erase(it, entries_.end()); + dirty_ = true; + return true; + } - void execute(Context& ctx) { - ensureSorted(); - executeAt(0, ctx); - } + void execute(Context& ctx) { + ensureSorted(); + executeAt(0, ctx); + } - size_t handlerCount() const { return entries_.size(); } + size_t handlerCount() const { return entries_.size(); } -private: - struct Entry { - std::string name; - Handler handler; - int priority; - size_t order; // insertion order for stable sorting - }; + private: + struct Entry { + std::string name; + Handler handler; + int priority; + size_t order; // insertion order for stable sorting + }; - void ensureSorted() { - if (!dirty_) - return; - std::stable_sort(entries_.begin(), entries_.end(), - [](const Entry& a, const Entry& b) { - if (a.priority != b.priority) - return a.priority < b.priority; - return a.order < b.order; - }); - dirty_ = false; - } + void ensureSorted() { + if (!dirty_) + return; + std::stable_sort(entries_.begin(), entries_.end(), [](const Entry& a, const Entry& b) { + if (a.priority != b.priority) + return a.priority < b.priority; + return a.order < b.order; + }); + dirty_ = false; + } - void executeAt(size_t index, Context& ctx) { - if (index >= entries_.size()) - return; - auto& entry = entries_[index]; - entry.handler(ctx, [this, index, &ctx]() { executeAt(index + 1, ctx); }); - } + void executeAt(size_t index, Context& ctx) { + if (index >= entries_.size()) + return; + auto& entry = entries_[index]; + entry.handler(ctx, [this, index, &ctx]() { executeAt(index + 1, ctx); }); + } - std::vector entries_; - size_t insertOrder_ = 0; - bool dirty_ = false; + std::vector entries_; + size_t insertOrder_ = 0; + bool dirty_ = false; }; } // namespace fabric diff --git a/include/fabric/core/Plugin.hh b/include/fabric/core/Plugin.hh index 061811a4..248d4b3e 100644 --- a/include/fabric/core/Plugin.hh +++ b/include/fabric/core/Plugin.hh @@ -3,72 +3,72 @@ #include "fabric/core/Component.hh" #include #include +#include #include #include #include -#include namespace fabric { /** * @brief Interface for plugins in the Fabric framework - * + * * Plugins extend the functionality of the Fabric framework by providing * additional components, services, or functionality. */ class Plugin { -public: - /** - * @brief Virtual destructor - */ - virtual ~Plugin() = default; - - /** - * @brief Get the plugin name - * - * @return Plugin name - */ - virtual std::string getName() const = 0; - - /** - * @brief Get the plugin version - * - * @return Plugin version - */ - virtual std::string getVersion() const = 0; - - /** - * @brief Get the plugin author - * - * @return Plugin author - */ - virtual std::string getAuthor() const = 0; - - /** - * @brief Get the plugin description - * - * @return Plugin description - */ - virtual std::string getDescription() const = 0; - - /** - * @brief Initialize the plugin - * - * @return true if initialization succeeded, false otherwise - */ - virtual bool initialize() = 0; - - /** - * @brief Shut down the plugin - */ - virtual void shutdown() = 0; - - /** - * @brief Get the components provided by this plugin - * - * @return Vector of components - */ - virtual std::vector> getComponents() = 0; + public: + /** + * @brief Virtual destructor + */ + virtual ~Plugin() = default; + + /** + * @brief Get the plugin name + * + * @return Plugin name + */ + virtual std::string getName() const = 0; + + /** + * @brief Get the plugin version + * + * @return Plugin version + */ + virtual std::string getVersion() const = 0; + + /** + * @brief Get the plugin author + * + * @return Plugin author + */ + virtual std::string getAuthor() const = 0; + + /** + * @brief Get the plugin description + * + * @return Plugin description + */ + virtual std::string getDescription() const = 0; + + /** + * @brief Initialize the plugin + * + * @return true if initialization succeeded, false otherwise + */ + virtual bool initialize() = 0; + + /** + * @brief Shut down the plugin + */ + virtual void shutdown() = 0; + + /** + * @brief Get the components provided by this plugin + * + * @return Vector of components + */ + virtual std::vector> getComponents() = 0; }; /** @@ -78,79 +78,79 @@ using PluginFactory = std::function()>; /** * @brief Manages plugins in the Fabric framework - * + * * The PluginManager keeps track of loaded plugins and provides * methods for loading, unloading, and accessing plugins. - * + * * Thread safety: All methods are thread-safe. */ class PluginManager { -public: - PluginManager() = default; - - /** - * @brief Register a plugin factory - * - * @param name Plugin name - * @param factory Plugin factory function - * @throws FabricException if name is empty, factory is null, or plugin is already registered - */ - void registerPlugin(const std::string& name, const PluginFactory& factory); - - /** - * @brief Load a plugin by name - * - * @param name Plugin name - * @return true if the plugin was loaded, false otherwise - */ - bool loadPlugin(const std::string& name); - - /** - * @brief Unload a plugin by name - * - * @param name Plugin name - * @return true if the plugin was unloaded, false otherwise - */ - bool unloadPlugin(const std::string& name); - - /** - * @brief Get a plugin by name - * - * @param name Plugin name - * @return Plugin or nullptr if not found - */ - std::shared_ptr getPlugin(const std::string& name) const; - - /** - * @brief Get all loaded plugins - * - * @return Map of plugin names to plugins - * @note This returns a copy to ensure thread safety - */ - std::unordered_map> getPlugins() const; - - /** - * @brief Initialize all loaded plugins - * - * @return true if all plugins initialized successfully, false otherwise - */ - bool initializeAll(); - - /** - * @brief Shut down all loaded plugins - * - * This method shuts down plugins in reverse dependency order - * to ensure proper cleanup. - */ - void shutdownAll(); - - PluginManager(const PluginManager&) = delete; - PluginManager& operator=(const PluginManager&) = delete; - -private: - mutable std::mutex pluginMutex; - std::unordered_map pluginFactories; - std::unordered_map> loadedPlugins; + public: + PluginManager() = default; + + /** + * @brief Register a plugin factory + * + * @param name Plugin name + * @param factory Plugin factory function + * @throws FabricException if name is empty, factory is null, or plugin is already registered + */ + void registerPlugin(const std::string& name, const PluginFactory& factory); + + /** + * @brief Load a plugin by name + * + * @param name Plugin name + * @return true if the plugin was loaded, false otherwise + */ + bool loadPlugin(const std::string& name); + + /** + * @brief Unload a plugin by name + * + * @param name Plugin name + * @return true if the plugin was unloaded, false otherwise + */ + bool unloadPlugin(const std::string& name); + + /** + * @brief Get a plugin by name + * + * @param name Plugin name + * @return Plugin or nullptr if not found + */ + std::shared_ptr getPlugin(const std::string& name) const; + + /** + * @brief Get all loaded plugins + * + * @return Map of plugin names to plugins + * @note This returns a copy to ensure thread safety + */ + std::unordered_map> getPlugins() const; + + /** + * @brief Initialize all loaded plugins + * + * @return true if all plugins initialized successfully, false otherwise + */ + bool initializeAll(); + + /** + * @brief Shut down all loaded plugins + * + * This method shuts down plugins in reverse dependency order + * to ensure proper cleanup. + */ + void shutdownAll(); + + PluginManager(const PluginManager&) = delete; + PluginManager& operator=(const PluginManager&) = delete; + + private: + mutable std::mutex pluginMutex; + std::unordered_map pluginFactories; + std::unordered_map> loadedPlugins; }; /** @@ -161,12 +161,8 @@ private: * @param manager PluginManager reference * @param PluginClass Plugin class name */ -#define FABRIC_REGISTER_PLUGIN(manager, PluginClass) \ - (manager).registerPlugin( \ - #PluginClass, \ - []() -> std::shared_ptr { \ - return std::make_shared(); \ - } \ - ) - -} // namespace fabric \ No newline at end of file +#define FABRIC_REGISTER_PLUGIN(manager, PluginClass) \ + (manager).registerPlugin(#PluginClass, \ + []() -> std::shared_ptr { return std::make_shared(); }) + +} // namespace fabric diff --git a/include/fabric/core/Rendering.hh b/include/fabric/core/Rendering.hh index 41650790..5e37bff7 100644 --- a/include/fabric/core/Rendering.hh +++ b/include/fabric/core/Rendering.hh @@ -1,11 +1,11 @@ #pragma once -#include +#include "fabric/core/Spatial.hh" +#include #include #include -#include #include -#include "fabric/core/Spatial.hh" +#include namespace fabric { @@ -35,7 +35,7 @@ struct Plane { void normalize(); }; -enum class CullResult { +enum class CullResult : std::uint8_t { Inside, Outside, Intersect @@ -66,7 +66,7 @@ struct DrawCall { // Sorted collection of draw calls per view class RenderList { -public: + public: void addDrawCall(const DrawCall& call); void sortByKey(); void clear(); @@ -75,28 +75,21 @@ public: size_t size() const; bool empty() const; -private: + private: std::vector drawCalls_; }; // Transform interpolation using slerp (rotation) + lerp (position, scale) struct TransformInterpolator { - static Transform interpolate( - const Transform& prev, - const Transform& current, - float alpha - ); + static Transform interpolate(const Transform& prev, const Transform& current, float alpha); }; // Frustum-cull scene entities against a view-projection matrix. // Iterates entities with SceneEntity tag. Entities without BoundingBox // are always considered visible. class FrustumCuller { -public: - static std::vector cull( - const float* viewProjection, - flecs::world& world - ); + public: + static std::vector cull(const float* viewProjection, flecs::world& world); }; } // namespace fabric diff --git a/include/fabric/core/Resource.hh b/include/fabric/core/Resource.hh index 2b03754f..893479cd 100644 --- a/include/fabric/core/Resource.hh +++ b/include/fabric/core/Resource.hh @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -13,296 +14,283 @@ namespace fabric { /** * @brief State of a resource in the resource management system */ -enum class ResourceState { - Unloaded, // Resource is not loaded - Loading, // Resource is currently being loaded - Loaded, // Resource is fully loaded and ready to use - LoadingFailed, // Resource failed to load - Unloading // Resource is being unloaded +enum class ResourceState : std::uint8_t { + Unloaded, // Resource is not loaded + Loading, // Resource is currently being loaded + Loaded, // Resource is fully loaded and ready to use + LoadingFailed, // Resource failed to load + Unloading // Resource is being unloaded }; /** * @brief Priority of a resource load operation */ -enum class ResourcePriority { - Lowest, // Background loading, lowest priority - Low, // Lower than normal priority - Normal, // Default priority for most resources - High, // Higher than normal priority - Highest // Critical resources, highest priority +enum class ResourcePriority : std::uint8_t { + Lowest, // Background loading, lowest priority + Low, // Lower than normal priority + Normal, // Default priority for most resources + High, // Higher than normal priority + Highest // Critical resources, highest priority }; /** * @brief Base class for all resource types - * + * * Resources are assets that can be loaded, unloaded, and managed * by the resource management system. */ class Resource { -public: - /** - * @brief Constructor - * - * @param id Unique identifier for this resource - */ - explicit Resource(std::string id) - : id_(std::move(id)), state_(ResourceState::Unloaded) {} - - /** - * @brief Virtual destructor - */ - virtual ~Resource() = default; - - /** - * @brief Get the resource ID - * - * @return Resource ID - */ - const std::string& getId() const { return id_; } - - /** - * @brief Get the current state of the resource - * - * @return Resource state - */ - ResourceState getState() const { - std::lock_guard lock(mutex_); - return state_; - } - - /** - * @brief Get the current load count of the resource - * - * @return The number of times the resource has been loaded without being unloaded - */ - int getLoadCount() const { - std::lock_guard lock(mutex_); - return loadCount_; - } - - /** - * @brief Get the estimated memory usage of the resource in bytes - * - * @return Memory usage in bytes - */ - virtual size_t getMemoryUsage() const = 0; - - /** - * @brief Load the resource - * - * This method loads the resource synchronously. - * - * @return true if the resource was loaded successfully - */ - bool load() { - { - std::lock_guard lock(mutex_); - if (state_ == ResourceState::Loaded) { - // Resource is already loaded, just increment the load count - loadCount_++; - return true; - } - state_ = ResourceState::Loading; + public: + /** + * @brief Constructor + * + * @param id Unique identifier for this resource + */ + explicit Resource(std::string id) : id_(std::move(id)), state_(ResourceState::Unloaded) {} + + /** + * @brief Virtual destructor + */ + virtual ~Resource() = default; + + /** + * @brief Get the resource ID + * + * @return Resource ID + */ + const std::string& getId() const { return id_; } + + /** + * @brief Get the current state of the resource + * + * @return Resource state + */ + ResourceState getState() const { + std::lock_guard lock(mutex_); + return state_; } - - bool success = loadImpl(); - - { - std::lock_guard lock(mutex_); - if (success) { - state_ = ResourceState::Loaded; - loadCount_++; - } else { - state_ = ResourceState::LoadingFailed; - } + + /** + * @brief Get the current load count of the resource + * + * @return The number of times the resource has been loaded without being unloaded + */ + int getLoadCount() const { + std::lock_guard lock(mutex_); + return loadCount_; } - - return success; - } - - /** - * @brief Unload the resource - * - * This method unloads the resource, freeing associated memory. - */ - void unload() { - bool shouldUnload = false; - { - std::lock_guard lock(mutex_); - if (state_ == ResourceState::Unloaded) { - return; - } - - // Decrement load count, only actually unload when it reaches 0 - if (loadCount_ > 0) { - loadCount_--; - } - - if (loadCount_ == 0) { - state_ = ResourceState::Unloading; - shouldUnload = true; - } + + /** + * @brief Get the estimated memory usage of the resource in bytes + * + * @return Memory usage in bytes + */ + virtual size_t getMemoryUsage() const = 0; + + /** + * @brief Load the resource + * + * This method loads the resource synchronously. + * + * @return true if the resource was loaded successfully + */ + bool load() { + { + std::lock_guard lock(mutex_); + if (state_ == ResourceState::Loaded) { + // Resource is already loaded, just increment the load count + loadCount_++; + return true; + } + state_ = ResourceState::Loading; + } + + bool success = loadImpl(); + + { + std::lock_guard lock(mutex_); + if (success) { + state_ = ResourceState::Loaded; + loadCount_++; + } else { + state_ = ResourceState::LoadingFailed; + } + } + + return success; } - - // Only call unloadImpl if we're actually unloading - if (shouldUnload) { - unloadImpl(); - - std::lock_guard lock(mutex_); - state_ = ResourceState::Unloaded; + + /** + * @brief Unload the resource + * + * This method unloads the resource, freeing associated memory. + */ + void unload() { + bool shouldUnload = false; + { + std::lock_guard lock(mutex_); + if (state_ == ResourceState::Unloaded) { + return; + } + + // Decrement load count, only actually unload when it reaches 0 + if (loadCount_ > 0) { + loadCount_--; + } + + if (loadCount_ == 0) { + state_ = ResourceState::Unloading; + shouldUnload = true; + } + } + + // Only call unloadImpl if we're actually unloading + if (shouldUnload) { + unloadImpl(); + + std::lock_guard lock(mutex_); + state_ = ResourceState::Unloaded; + } } - } - -protected: - /** - * @brief Implementation of the resource loading logic - * - * @return true if loading succeeded - */ - virtual bool loadImpl() = 0; - - /** - * @brief Implementation of the resource unloading logic - */ - virtual void unloadImpl() = 0; - -private: - std::string id_; - ResourceState state_; - mutable std::mutex mutex_; - int loadCount_ = 0; // Track how many times load() has been called without unload() + + protected: + /** + * @brief Implementation of the resource loading logic + * + * @return true if loading succeeded + */ + virtual bool loadImpl() = 0; + + /** + * @brief Implementation of the resource unloading logic + */ + virtual void unloadImpl() = 0; + + private: + std::string id_; + ResourceState state_; + mutable std::mutex mutex_; + int loadCount_ = 0; // Track how many times load() has been called without unload() }; /** * @brief Factory for creating resources of different types */ class ResourceFactory { -public: - /** - * @brief Register a factory function for a resource type - * - * @tparam T Resource type - * @param typeId Type identifier - * @param factory Factory function - */ - template - static void registerType(const std::string& typeId, std::function(const std::string&)> factory) { - std::lock_guard lock(mutex_); - factories_[typeId] = [factory](const std::string& id) { - return std::static_pointer_cast(factory(id)); - }; - } - - /** - * @brief Create a resource of the specified type - * - * @param typeId Type identifier - * @param id Resource identifier - * @return Shared pointer to the created resource, or nullptr if the type is not registered - */ - static std::shared_ptr create(const std::string& typeId, const std::string& id); - - /** - * @brief Check if a resource type is registered - * - * @param typeId Type identifier - * @return true if the type is registered - */ - static bool isTypeRegistered(const std::string& typeId); - -private: - static std::mutex mutex_; - static std::unordered_map(const std::string&)>> factories_; + public: + /** + * @brief Register a factory function for a resource type + * + * @tparam T Resource type + * @param typeId Type identifier + * @param factory Factory function + */ + template + static void registerType(const std::string& typeId, std::function(const std::string&)> factory) { + std::lock_guard lock(mutex_); + factories_[typeId] = [factory](const std::string& id) { + return std::static_pointer_cast(factory(id)); + }; + } + + /** + * @brief Create a resource of the specified type + * + * @param typeId Type identifier + * @param id Resource identifier + * @return Shared pointer to the created resource, or nullptr if the type is not registered + */ + static std::shared_ptr create(const std::string& typeId, const std::string& id); + + /** + * @brief Check if a resource type is registered + * + * @param typeId Type identifier + * @return true if the type is registered + */ + static bool isTypeRegistered(const std::string& typeId); + + private: + static std::mutex mutex_; + static std::unordered_map(const std::string&)>> factories_; }; /** * @brief A reference-counted handle to a resource - * + * * ResourceHandle provides safe access to resources managed by the ResourceHub. * It automatically maintains reference counting and ensures resources are loaded when needed. - * + * * @tparam T The resource type */ -template -class ResourceHandle { -public: - /** - * @brief Default constructor - creates an empty handle - */ - ResourceHandle() = default; - - /** - * @brief Construct from a resource pointer - * - * @param resource Pointer to the resource - */ - explicit ResourceHandle(std::shared_ptr resource) - : resource_(std::move(resource)) {} - - /** - * @brief Get the resource pointer - * - * @return Pointer to the resource, or nullptr if the handle is empty - */ - T* get() const { - return resource_.get(); - } - - /** - * @brief Access the resource via arrow operator - * - * @return Pointer to the resource - */ - T* operator->() const { - return get(); - } - - /** - * @brief Check if the handle contains a valid resource - * - * @return true if the handle is not empty - */ - explicit operator bool() const { - return resource_ != nullptr; - } - - /** - * @brief Get the resource ID - * - * @return Resource ID, or empty string if the handle is empty - */ - std::string getId() const { - return resource_ ? resource_->getId() : ""; - } - - /** - * @brief Reset the resource handle, releasing the reference - */ - void reset() { - resource_.reset(); - } - -private: - std::shared_ptr resource_; +template class ResourceHandle { + public: + /** + * @brief Default constructor - creates an empty handle + */ + ResourceHandle() = default; + + /** + * @brief Construct from a resource pointer + * + * @param resource Pointer to the resource + */ + explicit ResourceHandle(std::shared_ptr resource) : resource_(std::move(resource)) {} + + /** + * @brief Get the resource pointer + * + * @return Pointer to the resource, or nullptr if the handle is empty + */ + T* get() const { return resource_.get(); } + + /** + * @brief Access the resource via arrow operator + * + * @return Pointer to the resource + */ + T* operator->() const { return get(); } + + /** + * @brief Check if the handle contains a valid resource + * + * @return true if the handle is not empty + */ + explicit operator bool() const { return resource_ != nullptr; } + + /** + * @brief Get the resource ID + * + * @return Resource ID, or empty string if the handle is empty + */ + std::string getId() const { return resource_ ? resource_->getId() : ""; } + + /** + * @brief Reset the resource handle, releasing the reference + */ + void reset() { resource_.reset(); } + + private: + std::shared_ptr resource_; }; /** * @brief Load request for the resource manager */ struct ResourceLoadRequest { - std::string typeId; - std::string resourceId; - ResourcePriority priority; - std::function)> callback; + std::string typeId; + std::string resourceId; + ResourcePriority priority; + std::function)> callback; }; /** * @brief Comparator for prioritizing load requests */ struct ResourceLoadRequestComparator { - bool operator()(const ResourceLoadRequest& a, const ResourceLoadRequest& b) const { - return static_cast(a.priority) < static_cast(b.priority); - } + bool operator()(const ResourceLoadRequest& a, const ResourceLoadRequest& b) const { + return static_cast(a.priority) < static_cast(b.priority); + } }; -} // namespace fabric \ No newline at end of file +} // namespace fabric diff --git a/include/fabric/core/ResourceHub.hh b/include/fabric/core/ResourceHub.hh index a0c16632..db5d10eb 100644 --- a/include/fabric/core/ResourceHub.hh +++ b/include/fabric/core/ResourceHub.hh @@ -1,8 +1,8 @@ #pragma once -#include "fabric/core/Resource.hh" -#include "fabric/utils/CoordinatedGraph.hh" #include "fabric/core/Log.hh" +#include "fabric/core/Resource.hh" +#include "fabric/utils/CoordinatedGraph.hh" #include #include #include @@ -22,7 +22,7 @@ namespace fabric { // Forward declarations namespace Test { - class ResourceHubTestHelper; +class ResourceHubTestHelper; } /** @@ -33,522 +33,507 @@ namespace Test { * and asynchronous resource loading options. */ class ResourceHub { - // Allow test helper to access protected members - friend class fabric::Test::ResourceHubTestHelper; - -public: - ResourceHub(); - ~ResourceHub(); - - /** - * @brief Load a resource synchronously - * - * @tparam T Resource type - * @param typeId Type identifier - * @param resourceId Resource identifier - * @return ResourceHandle for the loaded resource - */ - template - ResourceHandle load(const std::string &typeId, - const std::string &resourceId) { - static_assert(std::is_base_of::value, - "T must be derived from Resource"); - - // Robust implementation following best practices from docs/guides/IMPLEMENTATION_PATTERNS.md - // Using the Copy-Then-Process pattern and avoiding nested locks - - const int LOAD_TIMEOUT_MS = 500; // 500ms timeout to prevent UI hangs - const int PHASE_TIMEOUT_MS = 150; // Each phase gets shorter timeout - auto startTime = std::chrono::steady_clock::now(); - - // Timeout checker function - auto isTimedOut = [&startTime, LOAD_TIMEOUT_MS]() -> bool { - return std::chrono::duration_cast( - std::chrono::steady_clock::now() - startTime) - .count() > LOAD_TIMEOUT_MS; - }; - - try { - // ================================================================= - // Phase 1: Resource Lookup or Creation - Highly resilient to failures - // ================================================================= - std::shared_ptr resource; - bool createdNewResource = false; - - // First attempt to get existing resource - if (!isTimedOut()) { + // Allow test helper to access protected members + friend class fabric::Test::ResourceHubTestHelper; + + public: + ResourceHub(); + ~ResourceHub(); + + /** + * @brief Load a resource synchronously + * + * @tparam T Resource type + * @param typeId Type identifier + * @param resourceId Resource identifier + * @return ResourceHandle for the loaded resource + */ + template ResourceHandle load(const std::string& typeId, const std::string& resourceId) { + static_assert(std::is_base_of::value, "T must be derived from Resource"); + + // Robust implementation following best practices from docs/guides/IMPLEMENTATION_PATTERNS.md + // Using the Copy-Then-Process pattern and avoiding nested locks + + const int LOAD_TIMEOUT_MS = 500; // 500ms timeout to prevent UI hangs + const int PHASE_TIMEOUT_MS = 150; // Each phase gets shorter timeout + auto startTime = std::chrono::steady_clock::now(); + + // Timeout checker function + auto isTimedOut = [&startTime, LOAD_TIMEOUT_MS]() -> bool { + return std::chrono::duration_cast(std::chrono::steady_clock::now() - startTime) + .count() > LOAD_TIMEOUT_MS; + }; + try { - // Use a very short operation timeout to avoid blocking - bool nodeExists = false; - try { - nodeExists = resourceGraph_.hasNode(resourceId); - } catch (...) { - // Silently continue with creation flow if this fails - } - - if (nodeExists) { - try { - auto node = resourceGraph_.getNode(resourceId, PHASE_TIMEOUT_MS / 3); - if (node) { - auto nodeLock = node->tryLock( - CoordinatedGraph>::LockIntent::Read, - PHASE_TIMEOUT_MS / 3); - - if (nodeLock && nodeLock->isLocked()) { - resource = nodeLock->getNode()->getDataNoLock(); - nodeLock->release(); + // ================================================================= + // Phase 1: Resource Lookup or Creation - Highly resilient to failures + // ================================================================= + std::shared_ptr resource; + bool createdNewResource = false; + + // First attempt to get existing resource + if (!isTimedOut()) { + try { + // Use a very short operation timeout to avoid blocking + bool nodeExists = false; + try { + nodeExists = resourceGraph_.hasNode(resourceId); + } catch (...) { + // Silently continue with creation flow if this fails + } + + if (nodeExists) { + try { + auto node = resourceGraph_.getNode(resourceId, PHASE_TIMEOUT_MS / 3); + if (node) { + auto nodeLock = + node->tryLock(CoordinatedGraph>::LockIntent::Read, + PHASE_TIMEOUT_MS / 3); + + if (nodeLock && nodeLock->isLocked()) { + resource = nodeLock->getNode()->getDataNoLock(); + nodeLock->release(); + } + } + } catch (...) { + // Silently continue if this fails + } + } + } catch (...) { + // Silently continue with creation flow if this fails } - } - } catch (...) { - // Silently continue if this fails } - } - } catch (...) { - // Silently continue with creation flow if this fails - } - } - - // If we still don't have a resource by this point, create a new one - if (!resource && !isTimedOut()) { - try { - // Create new resource - resource = ResourceFactory::create(typeId, resourceId); - if (resource) { - createdNewResource = true; - - // Try to add resource to graph with strict timeout - try { - bool added = resourceGraph_.addNode(resourceId, resource); - - // If adding failed, the node might already exist - if (!added) { - // Handle the case where someone else added the node first + + // If we still don't have a resource by this point, create a new one + if (!resource && !isTimedOut()) { try { - auto node = resourceGraph_.getNode(resourceId, PHASE_TIMEOUT_MS / 3); - if (node) { - auto nodeLock = node->tryLock( - CoordinatedGraph>::LockIntent::Read, - PHASE_TIMEOUT_MS / 3); - - if (nodeLock && nodeLock->isLocked()) { - resource = nodeLock->getNode()->getDataNoLock(); - createdNewResource = false; - nodeLock->release(); + // Create new resource + resource = ResourceFactory::create(typeId, resourceId); + if (resource) { + createdNewResource = true; + + // Try to add resource to graph with strict timeout + try { + bool added = resourceGraph_.addNode(resourceId, resource); + + // If adding failed, the node might already exist + if (!added) { + // Handle the case where someone else added the node first + try { + auto node = resourceGraph_.getNode(resourceId, PHASE_TIMEOUT_MS / 3); + if (node) { + auto nodeLock = + node->tryLock(CoordinatedGraph>::LockIntent::Read, + PHASE_TIMEOUT_MS / 3); + + if (nodeLock && nodeLock->isLocked()) { + resource = nodeLock->getNode()->getDataNoLock(); + createdNewResource = false; + nodeLock->release(); + } + } + } catch (...) { + // If we can't get the node someone else added, keep our locally created one + // We just won't be able to add it to the graph + } + } + } catch (...) { + // If graph operations fail, we still have the resource locally + // Just continue with local instance + FABRIC_LOG_WARN("Failed to add resource to graph: {}", resourceId); + } + } else { + FABRIC_LOG_ERROR("Failed to create resource: {}", resourceId); } - } + } catch (const std::exception& e) { + FABRIC_LOG_ERROR("Exception creating resource: {}", e.what()); } catch (...) { - // If we can't get the node someone else added, keep our locally created one - // We just won't be able to add it to the graph + FABRIC_LOG_ERROR("Unknown exception creating resource"); } - } - } catch (...) { - // If graph operations fail, we still have the resource locally - // Just continue with local instance - FABRIC_LOG_WARN("Failed to add resource to graph: {}", resourceId); } - } else { - FABRIC_LOG_ERROR("Failed to create resource: {}", resourceId); - } - } catch (const std::exception &e) { - FABRIC_LOG_ERROR("Exception creating resource: {}", e.what()); - } catch (...) { - FABRIC_LOG_ERROR("Unknown exception creating resource"); - } - } - - // Return early if we have no resource or timed out - if (!resource) { - if (isTimedOut()) { - FABRIC_LOG_WARN("Timed out in ResourceHub::load during resource lookup for {}", resourceId); - } else { - FABRIC_LOG_ERROR("Could not create or retrieve resource: {}", resourceId); - } - return ResourceHandle(); - } - - // ================================================================= - // Phase 2: Resource Loading - With proper timeout handling - // ================================================================= - if (resource->getState() != ResourceState::Loaded && !isTimedOut()) { - auto loadTimeoutMs = PHASE_TIMEOUT_MS; - auto loadStartTime = std::chrono::steady_clock::now(); - - auto loadTimedOut = [&loadStartTime, loadTimeoutMs]() -> bool { - return std::chrono::duration_cast( - std::chrono::steady_clock::now() - loadStartTime) - .count() > loadTimeoutMs; - }; - - // Load the resource with a separate timeout - try { - // Create a future with timeout for loading. - // Promise is moved into the lambda so the detached thread - // owns it outright. Resource is captured by value (shared_ptr - // copy) so it stays alive even if this stack frame returns. - auto loadPromise = std::make_shared>(); - auto loadFuture = loadPromise->get_future(); - - std::shared_ptr resourceCopy = resource; - std::thread loadThread([resourceCopy, loadPromise]() { - try { - bool result = resourceCopy->load(); - loadPromise->set_value(result); - } catch (...) { - try { - loadPromise->set_value(false); - } catch (...) { - // Promise might already be satisfied - } + + // Return early if we have no resource or timed out + if (!resource) { + if (isTimedOut()) { + FABRIC_LOG_WARN("Timed out in ResourceHub::load during resource lookup for {}", resourceId); + } else { + FABRIC_LOG_ERROR("Could not create or retrieve resource: {}", resourceId); + } + return ResourceHandle(); } - }); - - loadThread.detach(); - - // Wait for the future with timeout - bool loadSuccess = false; - - if (loadFuture.wait_for(std::chrono::milliseconds(loadTimeoutMs)) == - std::future_status::ready) { - try { - loadSuccess = loadFuture.get(); - } catch (...) { - loadSuccess = false; + + // ================================================================= + // Phase 2: Resource Loading - With proper timeout handling + // ================================================================= + if (resource->getState() != ResourceState::Loaded && !isTimedOut()) { + auto loadTimeoutMs = PHASE_TIMEOUT_MS; + auto loadStartTime = std::chrono::steady_clock::now(); + + auto loadTimedOut = [&loadStartTime, loadTimeoutMs]() -> bool { + return std::chrono::duration_cast(std::chrono::steady_clock::now() - + loadStartTime) + .count() > loadTimeoutMs; + }; + + // Load the resource with a separate timeout + try { + // Create a future with timeout for loading. + // Promise is moved into the lambda so the detached thread + // owns it outright. Resource is captured by value (shared_ptr + // copy) so it stays alive even if this stack frame returns. + auto loadPromise = std::make_shared>(); + auto loadFuture = loadPromise->get_future(); + + std::shared_ptr resourceCopy = resource; + std::thread loadThread([resourceCopy, loadPromise]() { + try { + bool result = resourceCopy->load(); + loadPromise->set_value(result); + } catch (...) { + try { + loadPromise->set_value(false); + } catch (...) { + // Promise might already be satisfied + } + } + }); + + loadThread.detach(); + + // Wait for the future with timeout + bool loadSuccess = false; + + if (loadFuture.wait_for(std::chrono::milliseconds(loadTimeoutMs)) == std::future_status::ready) { + try { + loadSuccess = loadFuture.get(); + } catch (...) { + loadSuccess = false; + } + } else { + FABRIC_LOG_WARN("Resource loading timed out for: {}", resourceId); + loadSuccess = false; + } + + if (!loadSuccess) { + FABRIC_LOG_WARN("Failed to load resource: {}", resourceId); + // Continue anyway - we'll return the handle even if loading failed + } + + // Update access time if needed and if we haven't timed out + if ((createdNewResource || loadSuccess) && !loadTimedOut() && !isTimedOut()) { + try { + auto node = resourceGraph_.getNode(resourceId, PHASE_TIMEOUT_MS / 3); + if (node) { + node->touch(); + } + } catch (...) { + // Failure to update access time is not critical + } + } + } catch (const std::exception& e) { + FABRIC_LOG_ERROR("Exception during resource loading: {}", e.what()); + } catch (...) { + FABRIC_LOG_ERROR("Unknown exception during resource loading"); + } } - } else { - FABRIC_LOG_WARN("Resource loading timed out for: {}", resourceId); - loadSuccess = false; - } - - if (!loadSuccess) { - FABRIC_LOG_WARN("Failed to load resource: {}", resourceId); - // Continue anyway - we'll return the handle even if loading failed - } - - // Update access time if needed and if we haven't timed out - if ((createdNewResource || loadSuccess) && !loadTimedOut() && !isTimedOut()) { + + // ================================================================= + // Phase 3: Handle Creation + // ================================================================= try { - auto node = resourceGraph_.getNode(resourceId, PHASE_TIMEOUT_MS / 3); - if (node) { - node->touch(); - } - } catch (...) { - // Failure to update access time is not critical + // Even if loading failed, return a handle to the resource + // The client can check the resource state + if (!isTimedOut()) { + return ResourceHandle(std::static_pointer_cast(resource)); + } else { + FABRIC_LOG_WARN("Timed out before returning resource handle: {}", resourceId); + return ResourceHandle(); + } + } catch (const std::exception& e) { + FABRIC_LOG_ERROR("Exception creating resource handle: {}", e.what()); + return ResourceHandle(); } - } - } catch (const std::exception &e) { - FABRIC_LOG_ERROR("Exception during resource loading: {}", e.what()); + + } catch (const std::exception& e) { + FABRIC_LOG_ERROR("Exception in ResourceHub::load() for {}: {}", resourceId, e.what()); + return ResourceHandle(); } catch (...) { - FABRIC_LOG_ERROR("Unknown exception during resource loading"); - } - } - - // ================================================================= - // Phase 3: Handle Creation - // ================================================================= - try { - // Even if loading failed, return a handle to the resource - // The client can check the resource state - if (!isTimedOut()) { - return ResourceHandle(std::static_pointer_cast(resource)); - } else { - FABRIC_LOG_WARN("Timed out before returning resource handle: {}", resourceId); - return ResourceHandle(); + FABRIC_LOG_ERROR("Unknown exception in ResourceHub::load() for {}", resourceId); + return ResourceHandle(); } - } catch (const std::exception &e) { - FABRIC_LOG_ERROR("Exception creating resource handle: {}", e.what()); + + // Fallback in case of any uncaught errors return ResourceHandle(); - } - - } catch (const std::exception &e) { - FABRIC_LOG_ERROR("Exception in ResourceHub::load() for {}: {}", resourceId, e.what()); - return ResourceHandle(); - } catch (...) { - FABRIC_LOG_ERROR("Unknown exception in ResourceHub::load() for {}", resourceId); - return ResourceHandle(); } - - // Fallback in case of any uncaught errors - return ResourceHandle(); - } - - /** - * @brief Load a resource asynchronously - * - * @tparam T Resource type - * @param typeId Type identifier - * @param resourceId Resource identifier - * @param priority Loading priority - * @param callback Function to call when the resource is loaded - */ - template - void loadAsync(const std::string &typeId, const std::string &resourceId, - ResourcePriority priority, - std::function)> callback) { - static_assert(std::is_base_of::value, - "T must be derived from Resource"); - - // First check if the resource is already loaded - auto resourceNode = resourceGraph_.getNode(resourceId); - if (resourceNode) { - auto nodeLock = resourceNode->tryLock( - CoordinatedGraph>::LockIntent::Read); - if (nodeLock && nodeLock->isLocked()) { - auto resource = nodeLock->getNode()->getDataNoLock(); - if (resource->getState() == ResourceState::Loaded) { - if (callback) { - callback(ResourceHandle(std::static_pointer_cast(resource))); - } - return; + + /** + * @brief Load a resource asynchronously + * + * @tparam T Resource type + * @param typeId Type identifier + * @param resourceId Resource identifier + * @param priority Loading priority + * @param callback Function to call when the resource is loaded + */ + template + void loadAsync(const std::string& typeId, const std::string& resourceId, ResourcePriority priority, + std::function)> callback) { + static_assert(std::is_base_of::value, "T must be derived from Resource"); + + // First check if the resource is already loaded + auto resourceNode = resourceGraph_.getNode(resourceId); + if (resourceNode) { + auto nodeLock = resourceNode->tryLock(CoordinatedGraph>::LockIntent::Read); + if (nodeLock && nodeLock->isLocked()) { + auto resource = nodeLock->getNode()->getDataNoLock(); + if (resource->getState() == ResourceState::Loaded) { + if (callback) { + callback(ResourceHandle(std::static_pointer_cast(resource))); + } + return; + } + } } - } - } - // Create a load request - ResourceLoadRequest request; - request.typeId = typeId; - request.resourceId = resourceId; - request.priority = priority; + // Create a load request + ResourceLoadRequest request; + request.typeId = typeId; + request.resourceId = resourceId; + request.priority = priority; - if (callback) { - request.callback = [callback](std::shared_ptr resource) { - callback(ResourceHandle(std::static_pointer_cast(resource))); - }; - } + if (callback) { + request.callback = [callback](std::shared_ptr resource) { + callback(ResourceHandle(std::static_pointer_cast(resource))); + }; + } + + // Add the request to the queue + { + std::lock_guard lock(queueMutex_); + loadQueue_.push(request); + } - // Add the request to the queue - { - std::lock_guard lock(queueMutex_); - loadQueue_.push(request); + // Signal the worker thread + queueCondition_.notify_one(); } - // Signal the worker thread - queueCondition_.notify_one(); - } - - /** - * @brief Add a dependency between two resources - * - * @param dependentId ID of the dependent resource - * @param dependencyId ID of the dependency - * @return true if dependency was added, false if either resource doesn't - * exist or dependecy already exists - */ - bool addDependency(const std::string &dependentId, - const std::string &dependencyId); - - /** - * @brief Remove a dependency between two resources - * - * @param dependentId ID of the dependent resource - * @param dependencyId ID of the dependency - * @return true if dependency was removed, false if either resource doesn't - * exist or there was no dependency - */ - bool removeDependency(const std::string &dependentId, - const std::string &dependencyId); - - /** - * @brief Unload a resource - * - * @param resourceId Resource identifier - * @return true if the resource was unloaded - */ - bool unload(const std::string &resourceId); - - /** - * @brief Unload a resource with an option to cascade unload dependencies - * - * @param resourceId Resource identifier - * @param cascade If true, also unload resources that depend on this one - * @return true if the resource was unloaded - */ - bool unload(const std::string &resourceId, bool cascade); - - /** - * @brief Unload a resource and all resources that depend on it - * - * @param resourceId Resource identifier - * @return true if the resource was unloaded - */ - bool unloadRecursive(const std::string &resourceId); - - /** - * @brief Preload a batch of resources asynchronously - * - * @param typeIds Type identifiers for each resource - * @param resourceIds Resource identifiers - * @param priority Loading priority - */ - void preload(const std::vector &typeIds, - const std::vector &resourceIds, - ResourcePriority priority = ResourcePriority::Low); - - /** - * @brief Set the memory budget for the resource manager - * - * @param bytes Memory budget in bytes - */ - void setMemoryBudget(size_t bytes); - - /** - * @brief Get the memory budget - * - * @return Memory budget in bytes - */ - size_t getMemoryBudget() const; - - /** - * @brief Get the current memory usage - * - * @return Memory usage in bytes - */ - size_t getMemoryUsage() const; - - /** - * @brief Explicitly trigger memory budget enforcement - * - * @return The number of resources evicted - */ - size_t enforceMemoryBudget(); - - /** - * @brief Disable worker threads for testing - */ - void disableWorkerThreadsForTesting(); - - /** - * @brief Restart worker threads after testing - */ - void restartWorkerThreadsAfterTesting(); - - /** - * @brief Get the number of worker threads - * - * @return Number of worker threads - */ - unsigned int getWorkerThreadCount() const; - - /** - * @brief Set the number of worker threads - * - * @param count Number of worker threads - */ - void setWorkerThreadCount(unsigned int count); - - /** - * @brief Get resources that depend on a specific resource - * - * @param resourceId Resource identifier - * @return Set of resource IDs that depend on the specified resource - */ - std::unordered_set getDependents(const std::string &resourceId); - - /** - * @brief Get resources that a specific resource depends on - * - * @param resourceId Resource identifier - * @return Set of resource IDs that the specified resource depends on - */ - std::unordered_set - getDependencies(const std::string &resourceId); - - /** - * @brief Check if a resource exists - * - * @param resourceId Resource identifier - * @return true if the resource exists - */ - bool hasResource(const std::string &resourceId); - - /** - * @brief Check if a resource is loaded - * - * @param resourceId Resource identifier - * @return true if the resource is loaded - */ - bool isLoaded(const std::string &resourceId) const; - - /** - * @brief Get dependent resources as a vector - * - * @param resourceId Resource identifier - * @return Vector of resource IDs that depend on the specified resource - */ - std::vector - getDependentResources(const std::string &resourceId) const; - - /** - * @brief Get dependency resources as a vector - * - * @param resourceId Resource identifier - * @return Vector of resource IDs that the specified resource depends on - */ - std::vector - getDependencyResources(const std::string &resourceId) const; - - /** - * @brief Clear all resources - * - * This method unloads and removes all resources from the manager. - */ - void clear(); - - /** - * @brief Reset the resource hub to a clean state - * - * This method is useful for testing. It: - * 1. Disables worker threads - * 2. Clears all resources - * 3. Resets the memory budget to the default value - */ - void reset(); - - /** - * @brief Check if the resource hub is empty - * - * @return true if the hub has no resources - */ - bool isEmpty() const; - - /** - * @brief Shutdown the resource manager - * - * This method stops all worker threads and unloads all resources. - * The ResourceHub will no longer be usable after this call. - */ - void shutdown(); - -protected: - // For testing access - would normally be private but we need it in tests - CoordinatedGraph> resourceGraph_; - -private: - // Process load queue function - void processLoadQueue(); - - // Worker thread function - void workerThreadFunc(); - - // Enforce budget - void enforceBudget(); - - // Memory management - std::atomic memoryBudget_; - - // Worker threads - std::atomic workerThreadCount_; - std::vector> workerThreads_; - - // Load queue - std::priority_queue, - ResourceLoadRequestComparator> - loadQueue_; - - // Synchronization with timed mutex support for safer thread management - std::timed_mutex queueMutex_; - std::timed_mutex threadControlMutex_; // Mutex for thread creation/destruction - std::condition_variable_any queueCondition_; - std::atomic shutdown_{false}; + /** + * @brief Add a dependency between two resources + * + * @param dependentId ID of the dependent resource + * @param dependencyId ID of the dependency + * @return true if dependency was added, false if either resource doesn't + * exist or dependecy already exists + */ + bool addDependency(const std::string& dependentId, const std::string& dependencyId); + + /** + * @brief Remove a dependency between two resources + * + * @param dependentId ID of the dependent resource + * @param dependencyId ID of the dependency + * @return true if dependency was removed, false if either resource doesn't + * exist or there was no dependency + */ + bool removeDependency(const std::string& dependentId, const std::string& dependencyId); + + /** + * @brief Unload a resource + * + * @param resourceId Resource identifier + * @return true if the resource was unloaded + */ + bool unload(const std::string& resourceId); + + /** + * @brief Unload a resource with an option to cascade unload dependencies + * + * @param resourceId Resource identifier + * @param cascade If true, also unload resources that depend on this one + * @return true if the resource was unloaded + */ + bool unload(const std::string& resourceId, bool cascade); + + /** + * @brief Unload a resource and all resources that depend on it + * + * @param resourceId Resource identifier + * @return true if the resource was unloaded + */ + bool unloadRecursive(const std::string& resourceId); + + /** + * @brief Preload a batch of resources asynchronously + * + * @param typeIds Type identifiers for each resource + * @param resourceIds Resource identifiers + * @param priority Loading priority + */ + void preload(const std::vector& typeIds, const std::vector& resourceIds, + ResourcePriority priority = ResourcePriority::Low); + + /** + * @brief Set the memory budget for the resource manager + * + * @param bytes Memory budget in bytes + */ + void setMemoryBudget(size_t bytes); + + /** + * @brief Get the memory budget + * + * @return Memory budget in bytes + */ + size_t getMemoryBudget() const; + + /** + * @brief Get the current memory usage + * + * @return Memory usage in bytes + */ + size_t getMemoryUsage() const; + + /** + * @brief Explicitly trigger memory budget enforcement + * + * @return The number of resources evicted + */ + size_t enforceMemoryBudget(); + + /** + * @brief Disable worker threads for testing + */ + void disableWorkerThreadsForTesting(); + + /** + * @brief Restart worker threads after testing + */ + void restartWorkerThreadsAfterTesting(); + + /** + * @brief Get the number of worker threads + * + * @return Number of worker threads + */ + unsigned int getWorkerThreadCount() const; + + /** + * @brief Set the number of worker threads + * + * @param count Number of worker threads + */ + void setWorkerThreadCount(unsigned int count); + + /** + * @brief Get resources that depend on a specific resource + * + * @param resourceId Resource identifier + * @return Set of resource IDs that depend on the specified resource + */ + std::unordered_set getDependents(const std::string& resourceId); + + /** + * @brief Get resources that a specific resource depends on + * + * @param resourceId Resource identifier + * @return Set of resource IDs that the specified resource depends on + */ + std::unordered_set getDependencies(const std::string& resourceId); + + /** + * @brief Check if a resource exists + * + * @param resourceId Resource identifier + * @return true if the resource exists + */ + bool hasResource(const std::string& resourceId); + + /** + * @brief Check if a resource is loaded + * + * @param resourceId Resource identifier + * @return true if the resource is loaded + */ + bool isLoaded(const std::string& resourceId) const; + + /** + * @brief Get dependent resources as a vector + * + * @param resourceId Resource identifier + * @return Vector of resource IDs that depend on the specified resource + */ + std::vector getDependentResources(const std::string& resourceId) const; + + /** + * @brief Get dependency resources as a vector + * + * @param resourceId Resource identifier + * @return Vector of resource IDs that the specified resource depends on + */ + std::vector getDependencyResources(const std::string& resourceId) const; + + /** + * @brief Clear all resources + * + * This method unloads and removes all resources from the manager. + */ + void clear(); + + /** + * @brief Reset the resource hub to a clean state + * + * This method is useful for testing. It: + * 1. Disables worker threads + * 2. Clears all resources + * 3. Resets the memory budget to the default value + */ + void reset(); + + /** + * @brief Check if the resource hub is empty + * + * @return true if the hub has no resources + */ + bool isEmpty() const; + + /** + * @brief Shutdown the resource manager + * + * This method stops all worker threads and unloads all resources. + * The ResourceHub will no longer be usable after this call. + */ + void shutdown(); + + protected: + // For testing access - would normally be private but we need it in tests + CoordinatedGraph> resourceGraph_; + + private: + // Process load queue function + void processLoadQueue(); + + // Worker thread function + void workerThreadFunc(); + + // Enforce budget + void enforceBudget(); + + // Memory management + std::atomic memoryBudget_; + + // Worker threads + std::atomic workerThreadCount_; + std::vector> workerThreads_; + + // Load queue + std::priority_queue, ResourceLoadRequestComparator> + loadQueue_; + + // Synchronization with timed mutex support for safer thread management + std::timed_mutex queueMutex_; + std::timed_mutex threadControlMutex_; // Mutex for thread creation/destruction + std::condition_variable_any queueCondition_; + std::atomic shutdown_{false}; }; } // namespace fabric diff --git a/include/fabric/core/SceneView.hh b/include/fabric/core/SceneView.hh index a382c994..d02392b2 100644 --- a/include/fabric/core/SceneView.hh +++ b/include/fabric/core/SceneView.hh @@ -2,8 +2,8 @@ #include "fabric/core/Camera.hh" #include "fabric/core/Rendering.hh" -#include #include +#include #include namespace fabric { @@ -12,7 +12,7 @@ namespace fabric { // Orchestrates the per-frame render pipeline: // update camera -> extract frustum -> cull -> build render list -> submit. class SceneView { -public: + public: SceneView(uint8_t viewId, Camera& camera, flecs::world& world); // Set clear color and flags for this view @@ -25,7 +25,7 @@ public: Camera& camera(); const std::vector& visibleEntities() const; -private: + private: uint8_t viewId_; Camera& camera_; flecs::world& world_; diff --git a/include/fabric/core/Simulation.hh b/include/fabric/core/Simulation.hh index baa9857d..51bfd07f 100644 --- a/include/fabric/core/Simulation.hh +++ b/include/fabric/core/Simulation.hh @@ -9,11 +9,10 @@ namespace fabric { -using SimRule = - std::function; +using SimRule = std::function; class SimulationHarness { -public: + public: SimulationHarness() = default; void registerRule(const std::string& name, SimRule rule); @@ -27,7 +26,7 @@ public: EssenceField& essence(); const EssenceField& essence() const; -private: + private: DensityField density_; EssenceField essence_; std::vector> rules_; diff --git a/include/fabric/core/Spatial.hh b/include/fabric/core/Spatial.hh index 335ecfaa..8f9b3fbc 100644 --- a/include/fabric/core/Spatial.hh +++ b/include/fabric/core/Spatial.hh @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -20,831 +21,714 @@ template class Transform; /** * @brief Type tags for different coordinate spaces - * + * * These tags are used to distinguish between different coordinate spaces * at compile time, preventing accidental mixing of spaces. */ namespace Space { - struct Local {}; // Object's local coordinate space - struct World {}; // World-space coordinates - struct Screen {}; // Screen-space coordinates - struct Parent {}; // Parent-space coordinates -} +struct Local {}; // Object's local coordinate space +struct World {}; // World-space coordinates +struct Screen {}; // Screen-space coordinates +struct Parent {}; // Parent-space coordinates +} // namespace Space /** * @brief 2D vector class with coordinate space type safety - * + * * @tparam T Numeric type (float, double, etc.) * @tparam Space Coordinate space tag */ -template -class Vector2 { -public: - T x, y; - - Vector2() : x(0), y(0) {} - Vector2(T x, T y) : x(x), y(y) {} - - // Operators with the same space - Vector2 operator+(const Vector2& other) const { - return Vector2(x + other.x, y + other.y); - } - - Vector2 operator-(const Vector2& other) const { - return Vector2(x - other.x, y - other.y); - } - - Vector2 operator*(T scalar) const { - return Vector2(x * scalar, y * scalar); - } - - Vector2 operator/(T scalar) const { - return Vector2(x / scalar, y / scalar); - } - - // Cannot mix different spaces - these operations are deleted - template - Vector2 operator+(const Vector2&) const = delete; - - template - Vector2 operator-(const Vector2&) const = delete; - - // Dot product - T dot(const Vector2& other) const { - return x * other.x + y * other.y; - } - - // Length calculations - T lengthSquared() const { - return x * x + y * y; - } - - T length() const { - return std::sqrt(lengthSquared()); - } - - // Normalization - Vector2 normalized() const { - T len = length(); - if (len == 0) return *this; - return *this / len; - } - - void normalize() { - T len = length(); - if (len == 0) return; - x /= len; - y /= len; - } - - // Space conversion function - template - Vector2 as() const { - return Vector2(x, y); - } +template class Vector2 { + public: + T x, y; + + Vector2() : x(0), y(0) {} + Vector2(T x, T y) : x(x), y(y) {} + + // Operators with the same space + Vector2 operator+(const Vector2& other) const { + return Vector2(x + other.x, y + other.y); + } + + Vector2 operator-(const Vector2& other) const { + return Vector2(x - other.x, y - other.y); + } + + Vector2 operator*(T scalar) const { return Vector2(x * scalar, y * scalar); } + + Vector2 operator/(T scalar) const { return Vector2(x / scalar, y / scalar); } + + // Cannot mix different spaces - these operations are deleted + template Vector2 operator+(const Vector2&) const = delete; + + template Vector2 operator-(const Vector2&) const = delete; + + // Dot product + T dot(const Vector2& other) const { return x * other.x + y * other.y; } + + // Length calculations + T lengthSquared() const { return x * x + y * y; } + + T length() const { return std::sqrt(lengthSquared()); } + + // Normalization + Vector2 normalized() const { + T len = length(); + if (len == 0) + return *this; + return *this / len; + } + + void normalize() { + T len = length(); + if (len == 0) + return; + x /= len; + y /= len; + } + + // Space conversion function + template Vector2 as() const { return Vector2(x, y); } }; /** * @brief 3D vector class with coordinate space type safety - * + * * @tparam T Numeric type (float, double, etc.) * @tparam Space Coordinate space tag */ -template -class Vector3 { -public: - T x, y, z; - - Vector3() : x(0), y(0), z(0) {} - Vector3(T x, T y, T z) : x(x), y(y), z(z) {} - - // Operators with the same space - Vector3 operator+(const Vector3& other) const { - return Vector3(x + other.x, y + other.y, z + other.z); - } - - Vector3 operator-(const Vector3& other) const { - return Vector3(x - other.x, y - other.y, z - other.z); - } - - Vector3 operator*(T scalar) const { - return Vector3(x * scalar, y * scalar, z * scalar); - } - - Vector3 operator/(T scalar) const { - return Vector3(x / scalar, y / scalar, z / scalar); - } - - // Cannot mix different spaces - these operations are deleted - template - Vector3 operator+(const Vector3&) const = delete; - - template - Vector3 operator-(const Vector3&) const = delete; - - // Dot product - T dot(const Vector3& other) const { - return x * other.x + y * other.y + z * other.z; - } - - // Cross product - Vector3 cross(const Vector3& other) const { - return Vector3( - y * other.z - z * other.y, - z * other.x - x * other.z, - x * other.y - y * other.x - ); - } - - // Length calculations - T lengthSquared() const { - return x * x + y * y + z * z; - } - - T length() const { - return std::sqrt(lengthSquared()); - } - - // Normalization - Vector3 normalized() const { - T len = length(); - if (len == 0) return *this; - return *this / len; - } - - void normalize() { - T len = length(); - if (len == 0) return; - x /= len; - y /= len; - z /= len; - } - - // Linear interpolation - static Vector3 lerp(const Vector3& a, const Vector3& b, T t) { - return Vector3( - a.x + t * (b.x - a.x), - a.y + t * (b.y - a.y), - a.z + t * (b.z - a.z) - ); - } - - // Space conversion function - template - Vector3 as() const { - return Vector3(x, y, z); - } +template class Vector3 { + public: + T x, y, z; + + Vector3() : x(0), y(0), z(0) {} + Vector3(T x, T y, T z) : x(x), y(y), z(z) {} + + // Operators with the same space + Vector3 operator+(const Vector3& other) const { + return Vector3(x + other.x, y + other.y, z + other.z); + } + + Vector3 operator-(const Vector3& other) const { + return Vector3(x - other.x, y - other.y, z - other.z); + } + + Vector3 operator*(T scalar) const { return Vector3(x * scalar, y * scalar, z * scalar); } + + Vector3 operator/(T scalar) const { return Vector3(x / scalar, y / scalar, z / scalar); } + + // Cannot mix different spaces - these operations are deleted + template Vector3 operator+(const Vector3&) const = delete; + + template Vector3 operator-(const Vector3&) const = delete; + + // Dot product + T dot(const Vector3& other) const { return x * other.x + y * other.y + z * other.z; } + + // Cross product + Vector3 cross(const Vector3& other) const { + return Vector3(y * other.z - z * other.y, z * other.x - x * other.z, x * other.y - y * other.x); + } + + // Length calculations + T lengthSquared() const { return x * x + y * y + z * z; } + + T length() const { return std::sqrt(lengthSquared()); } + + // Normalization + Vector3 normalized() const { + T len = length(); + if (len == 0) + return *this; + return *this / len; + } + + void normalize() { + T len = length(); + if (len == 0) + return; + x /= len; + y /= len; + z /= len; + } + + // Linear interpolation + static Vector3 lerp(const Vector3& a, const Vector3& b, T t) { + return Vector3(a.x + t * (b.x - a.x), a.y + t * (b.y - a.y), a.z + t * (b.z - a.z)); + } + + // Space conversion function + template Vector3 as() const { return Vector3(x, y, z); } }; /** * @brief 4D vector class with coordinate space type safety - * + * * @tparam T Numeric type (float, double, etc.) * @tparam Space Coordinate space tag */ -template -class Vector4 { -public: - T x, y, z, w; - - Vector4() : x(0), y(0), z(0), w(0) {} - Vector4(T x, T y, T z, T w) : x(x), y(y), z(z), w(w) {} - Vector4(const Vector3& v, T w) : x(v.x), y(v.y), z(v.z), w(w) {} - - // Operators with the same space - Vector4 operator+(const Vector4& other) const { - return Vector4(x + other.x, y + other.y, z + other.z, w + other.w); - } - - Vector4 operator-(const Vector4& other) const { - return Vector4(x - other.x, y - other.y, z - other.z, w - other.w); - } - - Vector4 operator*(T scalar) const { - return Vector4(x * scalar, y * scalar, z * scalar, w * scalar); - } - - Vector4 operator/(T scalar) const { - return Vector4(x / scalar, y / scalar, z / scalar, w / scalar); - } - - // Cannot mix different spaces - these operations are deleted - template - Vector4 operator+(const Vector4&) const = delete; - - template - Vector4 operator-(const Vector4&) const = delete; - - // Dot product - T dot(const Vector4& other) const { - return x * other.x + y * other.y + z * other.z + w * other.w; - } - - // Length calculations - T lengthSquared() const { - return x * x + y * y + z * z + w * w; - } - - T length() const { - return std::sqrt(lengthSquared()); - } - - // Normalization - Vector4 normalized() const { - T len = length(); - if (len == 0) return *this; - return *this / len; - } - - void normalize() { - T len = length(); - if (len == 0) return; - x /= len; - y /= len; - z /= len; - w /= len; - } - - // Conversion to Vector3 (drops w) - Vector3 xyz() const { - return Vector3(x, y, z); - } - - // Space conversion function - template - Vector4 as() const { - return Vector4(x, y, z, w); - } +template class Vector4 { + public: + T x, y, z, w; + + Vector4() : x(0), y(0), z(0), w(0) {} + Vector4(T x, T y, T z, T w) : x(x), y(y), z(z), w(w) {} + Vector4(const Vector3& v, T w) : x(v.x), y(v.y), z(v.z), w(w) {} + + // Operators with the same space + Vector4 operator+(const Vector4& other) const { + return Vector4(x + other.x, y + other.y, z + other.z, w + other.w); + } + + Vector4 operator-(const Vector4& other) const { + return Vector4(x - other.x, y - other.y, z - other.z, w - other.w); + } + + Vector4 operator*(T scalar) const { + return Vector4(x * scalar, y * scalar, z * scalar, w * scalar); + } + + Vector4 operator/(T scalar) const { + return Vector4(x / scalar, y / scalar, z / scalar, w / scalar); + } + + // Cannot mix different spaces - these operations are deleted + template Vector4 operator+(const Vector4&) const = delete; + + template Vector4 operator-(const Vector4&) const = delete; + + // Dot product + T dot(const Vector4& other) const { return x * other.x + y * other.y + z * other.z + w * other.w; } + + // Length calculations + T lengthSquared() const { return x * x + y * y + z * z + w * w; } + + T length() const { return std::sqrt(lengthSquared()); } + + // Normalization + Vector4 normalized() const { + T len = length(); + if (len == 0) + return *this; + return *this / len; + } + + void normalize() { + T len = length(); + if (len == 0) + return; + x /= len; + y /= len; + z /= len; + w /= len; + } + + // Conversion to Vector3 (drops w) + Vector3 xyz() const { return Vector3(x, y, z); } + + // Space conversion function + template Vector4 as() const { return Vector4(x, y, z, w); } }; /** * @brief Quaternion class for representing rotations - * + * * @tparam T Numeric type (float, double, etc.) */ -template -class Quaternion { -public: - T x, y, z, w; - - Quaternion() : x(0), y(0), z(0), w(1) {} - Quaternion(T x, T y, T z, T w) : x(x), y(y), z(z), w(w) {} - - // Create from axis angle - static Quaternion fromAxisAngle(const Vector3& axis, T angle) { - T halfAngle = angle * T(0.5); - T s = std::sin(halfAngle); - - return Quaternion( - axis.x * s, - axis.y * s, - axis.z * s, - std::cos(halfAngle) - ); - } - - // Create from Euler angles (in radians) - static Quaternion fromEulerAngles(T pitch, T yaw, T roll) { - T cy = std::cos(yaw * T(0.5)); - T sy = std::sin(yaw * T(0.5)); - T cp = std::cos(pitch * T(0.5)); - T sp = std::sin(pitch * T(0.5)); - T cr = std::cos(roll * T(0.5)); - T sr = std::sin(roll * T(0.5)); - - return Quaternion( - cy * sp * cr + sy * cp * sr, - cy * cp * sr - sy * sp * cr, - sy * cp * cr - cy * sp * sr, - cy * cp * cr + sy * sp * sr - ); - } - - // Quaternion multiplication - Quaternion operator*(const Quaternion& other) const { - return Quaternion( - w * other.x + x * other.w + y * other.z - z * other.y, - w * other.y - x * other.z + y * other.w + z * other.x, - w * other.z + x * other.y - y * other.x + z * other.w, - w * other.w - x * other.x - y * other.y - z * other.z - ); - } - - // Length operations - T lengthSquared() const { - return x * x + y * y + z * z + w * w; - } - - T length() const { - return std::sqrt(lengthSquared()); - } - - // Normalization - Quaternion normalized() const { - T len = length(); - if (len == 0) return *this; - return Quaternion(x / len, y / len, z / len, w / len); - } - - void normalize() { - T len = length(); - if (len == 0) return; - x /= len; - y /= len; - z /= len; - w /= len; - } - - // Conjugate - Quaternion conjugate() const { - return Quaternion(-x, -y, -z, w); - } - - // Inverse - Quaternion inverse() const { - T lenSq = lengthSquared(); - if (lenSq == 0) return *this; - T invLenSq = T(1) / lenSq; - return Quaternion(-x * invLenSq, -y * invLenSq, -z * invLenSq, w * invLenSq); - } - - // Spherical linear interpolation - static Quaternion slerp(const Quaternion& a, const Quaternion& b, T t) { - T dot = a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w; - - // Negate one quaternion to take shorter path - Quaternion b2 = b; - if (dot < T(0)) { - dot = -dot; - b2 = Quaternion(-b.x, -b.y, -b.z, -b.w); - } - - // Fall back to normalized lerp when quaternions are very close - if (dot > T(0.9995)) { - return Quaternion( - a.x + t * (b2.x - a.x), - a.y + t * (b2.y - a.y), - a.z + t * (b2.z - a.z), - a.w + t * (b2.w - a.w) - ).normalized(); - } - - T theta = std::acos(dot); - T sinTheta = std::sin(theta); - T wa = std::sin((T(1) - t) * theta) / sinTheta; - T wb = std::sin(t * theta) / sinTheta; - - return Quaternion( - wa * a.x + wb * b2.x, - wa * a.y + wb * b2.y, - wa * a.z + wb * b2.z, - wa * a.w + wb * b2.w - ); - } - - // Rotate a vector by this quaternion - template - Vector3 rotateVector(const Vector3& v) const { - Quaternion vQuat(v.x, v.y, v.z, 0); - Quaternion result = *this * vQuat * conjugate(); - return Vector3(result.x, result.y, result.z); - } - - // Convert to Matrix4x4 - Matrix4x4 toMatrix() const; - - // Convert to Euler angles (in radians) - Vector3 toEulerAngles() const { - Vector3 angles; - - // Roll (x-axis rotation) - T sinr_cosp = 2 * (w * x + y * z); - T cosr_cosp = 1 - 2 * (x * x + y * y); - angles.x = std::atan2(sinr_cosp, cosr_cosp); - - // Pitch (y-axis rotation) - T sinp = 2 * (w * y - z * x); - if (std::abs(sinp) >= 1) - angles.y = std::copysign(T(M_PI / 2), sinp); // Use 90 degrees if out of range - else - angles.y = std::asin(sinp); - - // Yaw (z-axis rotation) - T siny_cosp = 2 * (w * z + x * y); - T cosy_cosp = 1 - 2 * (y * y + z * z); - angles.z = std::atan2(siny_cosp, cosy_cosp); - - return angles; - } +template class Quaternion { + public: + T x, y, z, w; + + Quaternion() : x(0), y(0), z(0), w(1) {} + Quaternion(T x, T y, T z, T w) : x(x), y(y), z(z), w(w) {} + + // Create from axis angle + static Quaternion fromAxisAngle(const Vector3& axis, T angle) { + T halfAngle = angle * T(0.5); + T s = std::sin(halfAngle); + + return Quaternion(axis.x * s, axis.y * s, axis.z * s, std::cos(halfAngle)); + } + + // Create from Euler angles (in radians) + static Quaternion fromEulerAngles(T pitch, T yaw, T roll) { + T cy = std::cos(yaw * T(0.5)); + T sy = std::sin(yaw * T(0.5)); + T cp = std::cos(pitch * T(0.5)); + T sp = std::sin(pitch * T(0.5)); + T cr = std::cos(roll * T(0.5)); + T sr = std::sin(roll * T(0.5)); + + return Quaternion(cy * sp * cr + sy * cp * sr, cy * cp * sr - sy * sp * cr, sy * cp * cr - cy * sp * sr, + cy * cp * cr + sy * sp * sr); + } + + // Quaternion multiplication + Quaternion operator*(const Quaternion& other) const { + return Quaternion(w * other.x + x * other.w + y * other.z - z * other.y, + w * other.y - x * other.z + y * other.w + z * other.x, + w * other.z + x * other.y - y * other.x + z * other.w, + w * other.w - x * other.x - y * other.y - z * other.z); + } + + // Length operations + T lengthSquared() const { return x * x + y * y + z * z + w * w; } + + T length() const { return std::sqrt(lengthSquared()); } + + // Normalization + Quaternion normalized() const { + T len = length(); + if (len == 0) + return *this; + return Quaternion(x / len, y / len, z / len, w / len); + } + + void normalize() { + T len = length(); + if (len == 0) + return; + x /= len; + y /= len; + z /= len; + w /= len; + } + + // Conjugate + Quaternion conjugate() const { return Quaternion(-x, -y, -z, w); } + + // Inverse + Quaternion inverse() const { + T lenSq = lengthSquared(); + if (lenSq == 0) + return *this; + T invLenSq = T(1) / lenSq; + return Quaternion(-x * invLenSq, -y * invLenSq, -z * invLenSq, w * invLenSq); + } + + // Spherical linear interpolation + static Quaternion slerp(const Quaternion& a, const Quaternion& b, T t) { + T dot = a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w; + + // Negate one quaternion to take shorter path + Quaternion b2 = b; + if (dot < T(0)) { + dot = -dot; + b2 = Quaternion(-b.x, -b.y, -b.z, -b.w); + } + + // Fall back to normalized lerp when quaternions are very close + if (dot > T(0.9995)) { + return Quaternion(a.x + t * (b2.x - a.x), a.y + t * (b2.y - a.y), a.z + t * (b2.z - a.z), + a.w + t * (b2.w - a.w)) + .normalized(); + } + + T theta = std::acos(dot); + T sinTheta = std::sin(theta); + T wa = std::sin((T(1) - t) * theta) / sinTheta; + T wb = std::sin(t * theta) / sinTheta; + + return Quaternion(wa * a.x + wb * b2.x, wa * a.y + wb * b2.y, wa * a.z + wb * b2.z, wa * a.w + wb * b2.w); + } + + // Rotate a vector by this quaternion + template Vector3 rotateVector(const Vector3& v) const { + Quaternion vQuat(v.x, v.y, v.z, 0); + Quaternion result = *this * vQuat * conjugate(); + return Vector3(result.x, result.y, result.z); + } + + // Convert to Matrix4x4 + Matrix4x4 toMatrix() const; + + // Convert to Euler angles (in radians) + Vector3 toEulerAngles() const { + Vector3 angles; + + // Roll (x-axis rotation) + T sinr_cosp = 2 * (w * x + y * z); + T cosr_cosp = 1 - 2 * (x * x + y * y); + angles.x = std::atan2(sinr_cosp, cosr_cosp); + + // Pitch (y-axis rotation) + T sinp = 2 * (w * y - z * x); + if (std::abs(sinp) >= 1) + angles.y = std::copysign(std::numbers::pi_v / T(2), sinp); + else + angles.y = std::asin(sinp); + + // Yaw (z-axis rotation) + T siny_cosp = 2 * (w * z + x * y); + T cosy_cosp = 1 - 2 * (y * y + z * z); + angles.z = std::atan2(siny_cosp, cosy_cosp); + + return angles; + } }; /** * @brief 4x4 matrix class for transformations - * + * * @tparam T Numeric type (float, double, etc.) */ -template -class Matrix4x4 { -public: - // Matrix stored in column-major order (OpenGL style) - std::array elements; - - // Constructor - identity matrix by default - Matrix4x4() { - setIdentity(); - } - - // Constructor from array - explicit Matrix4x4(const std::array& data) : elements(data) {} - - // Set to identity matrix - void setIdentity() { - elements = { - 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1 - }; - } - - // Element access - T& operator()(int row, int col) { - return elements[col * 4 + row]; - } - - const T& operator()(int row, int col) const { - return elements[col * 4 + row]; - } - - // Matrix multiplication - Matrix4x4 operator*(const Matrix4x4& other) const { - Matrix4x4 result; - - for (int i = 0; i < 4; ++i) { - for (int j = 0; j < 4; ++j) { - result(i, j) = 0; - for (int k = 0; k < 4; ++k) { - result(i, j) += (*this)(i, k) * other(k, j); +template class Matrix4x4 { + public: + // Matrix stored in column-major order (OpenGL style) + std::array elements; + + // Constructor - identity matrix by default + Matrix4x4() { setIdentity(); } + + // Constructor from array + explicit Matrix4x4(const std::array& data) : elements(data) {} + + // Set to identity matrix + void setIdentity() { elements = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1}; } + + // Element access + T& operator()(int row, int col) { return elements[col * 4 + row]; } + + const T& operator()(int row, int col) const { return elements[col * 4 + row]; } + + // Matrix multiplication + Matrix4x4 operator*(const Matrix4x4& other) const { + Matrix4x4 result; + + for (int i = 0; i < 4; ++i) { + for (int j = 0; j < 4; ++j) { + result(i, j) = 0; + for (int k = 0; k < 4; ++k) { + result(i, j) += (*this)(i, k) * other(k, j); + } + } } - } - } - - return result; - } - - // Vector multiplication (homogeneous coordinates) - template - Vector4 operator*(const Vector4& v) const { - return Vector4( - elements[0] * v.x + elements[4] * v.y + elements[8] * v.z + elements[12] * v.w, - elements[1] * v.x + elements[5] * v.y + elements[9] * v.z + elements[13] * v.w, - elements[2] * v.x + elements[6] * v.y + elements[10] * v.z + elements[14] * v.w, - elements[3] * v.x + elements[7] * v.y + elements[11] * v.z + elements[15] * v.w - ); - } - - // Vector3 transformation with implicit w=1 - template - Vector3 transformPoint(const Vector3& v) const { - Vector4 result = this->template operator*(Vector4(v, 1)); - if (result.w != 0) { - return Vector3(result.x / result.w, result.y / result.w, result.z / result.w); - } else { - return Vector3(result.x, result.y, result.z); - } - } - - // Vector3 transformation with implicit w=0 (direction vectors) - template - Vector3 transformDirection(const Vector3& v) const { - Vector4 result = this->template operator*(Vector4(v, 0)); - return Vector3(result.x, result.y, result.z); - } - - // Create a translation matrix - static Matrix4x4 translation(const Vector3& v) { - Matrix4x4 result; - result(0, 3) = v.x; - result(1, 3) = v.y; - result(2, 3) = v.z; - return result; - } - - // Create a scale matrix - static Matrix4x4 scaling(const Vector3& v) { - Matrix4x4 result; - result(0, 0) = v.x; - result(1, 1) = v.y; - result(2, 2) = v.z; - return result; - } - - // Create a rotation matrix from quaternion - static Matrix4x4 rotation(const Quaternion& q) { - T xx = q.x * q.x; - T xy = q.x * q.y; - T xz = q.x * q.z; - T xw = q.x * q.w; - T yy = q.y * q.y; - T yz = q.y * q.z; - T yw = q.y * q.w; - T zz = q.z * q.z; - T zw = q.z * q.w; - - Matrix4x4 result; - result(0, 0) = 1 - 2 * (yy + zz); - result(0, 1) = 2 * (xy - zw); - result(0, 2) = 2 * (xz + yw); - - result(1, 0) = 2 * (xy + zw); - result(1, 1) = 1 - 2 * (xx + zz); - result(1, 2) = 2 * (yz - xw); - - result(2, 0) = 2 * (xz - yw); - result(2, 1) = 2 * (yz + xw); - result(2, 2) = 1 - 2 * (xx + yy); - - return result; - } - - // Create a perspective projection matrix - static Matrix4x4 perspective(T fovY, T aspect, T near, T far) { - Matrix4x4 result; - - T f = T(1) / std::tan(fovY / T(2)); - - result(0, 0) = f / aspect; - result(1, 1) = f; - result(2, 2) = (far + near) / (near - far); - result(2, 3) = (T(2) * far * near) / (near - far); - result(3, 2) = -T(1); - result(3, 3) = T(0); - - return result; - } - - // Create an orthographic projection matrix - static Matrix4x4 orthographic(T left, T right, T bottom, T top, T near, T far) { - Matrix4x4 result; - - result(0, 0) = T(2) / (right - left); - result(1, 1) = T(2) / (top - bottom); - result(2, 2) = T(2) / (near - far); - - result(0, 3) = (left + right) / (left - right); - result(1, 3) = (bottom + top) / (bottom - top); - result(2, 3) = (near + far) / (near - far); - - return result; - } - - // Create a look-at view matrix - static Matrix4x4 lookAt(const Vector3& eye, const Vector3& target, const Vector3& up) { - Vector3 f = (target - eye).normalized(); - Vector3 s = f.cross(up).normalized(); - Vector3 u = s.cross(f); - - Matrix4x4 result; - - result(0, 0) = s.x; - result(0, 1) = s.y; - result(0, 2) = s.z; - - result(1, 0) = u.x; - result(1, 1) = u.y; - result(1, 2) = u.z; - - result(2, 0) = -f.x; - result(2, 1) = -f.y; - result(2, 2) = -f.z; - - result(0, 3) = -s.dot(eye); - result(1, 3) = -u.dot(eye); - result(2, 3) = f.dot(eye); - - return result; - } - - Matrix4x4 inverse() const { - static_assert(std::is_same_v || std::is_same_v, - "Matrix4x4::inverse() only supported for float and double"); - - if constexpr (std::is_same_v) { - glm::mat4 glmMat = glm::make_mat4(elements.data()); - float det = glm::determinant(glmMat); - if (std::abs(det) < 1e-8f) { - return Matrix4x4(); - } - glm::mat4 glmInv = glm::inverse(glmMat); - Matrix4x4 result; - std::copy(glm::value_ptr(glmInv), glm::value_ptr(glmInv) + 16, result.elements.begin()); - return result; - } else { - glm::dmat4 glmMat = glm::make_mat4(elements.data()); - double det = glm::determinant(glmMat); - if (std::abs(det) < 1e-15) { - return Matrix4x4(); - } - glm::dmat4 glmInv = glm::inverse(glmMat); - Matrix4x4 result; - std::copy(glm::value_ptr(glmInv), glm::value_ptr(glmInv) + 16, result.elements.begin()); - return result; - } - } - - // Matrix transpose - Matrix4x4 transpose() const { - Matrix4x4 result; - for (int i = 0; i < 4; ++i) { - for (int j = 0; j < 4; ++j) { - result(i, j) = (*this)(j, i); - } - } - return result; - } + + return result; + } + + // Vector multiplication (homogeneous coordinates) + template + Vector4 operator*(const Vector4& v) const { + return Vector4( + elements[0] * v.x + elements[4] * v.y + elements[8] * v.z + elements[12] * v.w, + elements[1] * v.x + elements[5] * v.y + elements[9] * v.z + elements[13] * v.w, + elements[2] * v.x + elements[6] * v.y + elements[10] * v.z + elements[14] * v.w, + elements[3] * v.x + elements[7] * v.y + elements[11] * v.z + elements[15] * v.w); + } + + // Vector3 transformation with implicit w=1 + template + Vector3 transformPoint(const Vector3& v) const { + Vector4 result = + this->template operator* (Vector4(v, 1)); + if (result.w != 0) { + return Vector3(result.x / result.w, result.y / result.w, result.z / result.w); + } else { + return Vector3(result.x, result.y, result.z); + } + } + + // Vector3 transformation with implicit w=0 (direction vectors) + template + Vector3 transformDirection(const Vector3& v) const { + Vector4 result = + this->template operator* (Vector4(v, 0)); + return Vector3(result.x, result.y, result.z); + } + + // Create a translation matrix + static Matrix4x4 translation(const Vector3& v) { + Matrix4x4 result; + result(0, 3) = v.x; + result(1, 3) = v.y; + result(2, 3) = v.z; + return result; + } + + // Create a scale matrix + static Matrix4x4 scaling(const Vector3& v) { + Matrix4x4 result; + result(0, 0) = v.x; + result(1, 1) = v.y; + result(2, 2) = v.z; + return result; + } + + // Create a rotation matrix from quaternion + static Matrix4x4 rotation(const Quaternion& q) { + T xx = q.x * q.x; + T xy = q.x * q.y; + T xz = q.x * q.z; + T xw = q.x * q.w; + T yy = q.y * q.y; + T yz = q.y * q.z; + T yw = q.y * q.w; + T zz = q.z * q.z; + T zw = q.z * q.w; + + Matrix4x4 result; + result(0, 0) = 1 - 2 * (yy + zz); + result(0, 1) = 2 * (xy - zw); + result(0, 2) = 2 * (xz + yw); + + result(1, 0) = 2 * (xy + zw); + result(1, 1) = 1 - 2 * (xx + zz); + result(1, 2) = 2 * (yz - xw); + + result(2, 0) = 2 * (xz - yw); + result(2, 1) = 2 * (yz + xw); + result(2, 2) = 1 - 2 * (xx + yy); + + return result; + } + + // Create a perspective projection matrix + static Matrix4x4 perspective(T fovY, T aspect, T zNear, T zFar) { + Matrix4x4 result; + + T f = T(1) / std::tan(fovY / T(2)); + + result(0, 0) = f / aspect; + result(1, 1) = f; + result(2, 2) = (zFar + zNear) / (zNear - zFar); + result(2, 3) = (T(2) * zFar * zNear) / (zNear - zFar); + result(3, 2) = -T(1); + result(3, 3) = T(0); + + return result; + } + + // Create an orthographic projection matrix + static Matrix4x4 orthographic(T left, T right, T bottom, T top, T zNear, T zFar) { + Matrix4x4 result; + + result(0, 0) = T(2) / (right - left); + result(1, 1) = T(2) / (top - bottom); + result(2, 2) = T(2) / (zNear - zFar); + + result(0, 3) = (left + right) / (left - right); + result(1, 3) = (bottom + top) / (bottom - top); + result(2, 3) = (zNear + zFar) / (zNear - zFar); + + return result; + } + + // Create a look-at view matrix + static Matrix4x4 lookAt(const Vector3& eye, const Vector3& target, + const Vector3& up) { + Vector3 f = (target - eye).normalized(); + Vector3 s = f.cross(up).normalized(); + Vector3 u = s.cross(f); + + Matrix4x4 result; + + result(0, 0) = s.x; + result(0, 1) = s.y; + result(0, 2) = s.z; + + result(1, 0) = u.x; + result(1, 1) = u.y; + result(1, 2) = u.z; + + result(2, 0) = -f.x; + result(2, 1) = -f.y; + result(2, 2) = -f.z; + + result(0, 3) = -s.dot(eye); + result(1, 3) = -u.dot(eye); + result(2, 3) = f.dot(eye); + + return result; + } + + Matrix4x4 inverse() const { + static_assert(std::is_same_v || std::is_same_v, + "Matrix4x4::inverse() only supported for float and double"); + + if constexpr (std::is_same_v) { + glm::mat4 glmMat = glm::make_mat4(elements.data()); + float det = glm::determinant(glmMat); + if (std::abs(det) < 1e-8f) { + return Matrix4x4(); + } + glm::mat4 glmInv = glm::inverse(glmMat); + Matrix4x4 result; + std::copy(glm::value_ptr(glmInv), glm::value_ptr(glmInv) + 16, result.elements.begin()); + return result; + } else { + glm::dmat4 glmMat = glm::make_mat4(elements.data()); + double det = glm::determinant(glmMat); + if (std::abs(det) < 1e-15) { + return Matrix4x4(); + } + glm::dmat4 glmInv = glm::inverse(glmMat); + Matrix4x4 result; + std::copy(glm::value_ptr(glmInv), glm::value_ptr(glmInv) + 16, result.elements.begin()); + return result; + } + } + + // Matrix transpose + Matrix4x4 transpose() const { + Matrix4x4 result; + for (int i = 0; i < 4; ++i) { + for (int j = 0; j < 4; ++j) { + result(i, j) = (*this)(j, i); + } + } + return result; + } }; // Implementation of Quaternion::toMatrix now that Matrix4x4 is defined -template -Matrix4x4 Quaternion::toMatrix() const { - return Matrix4x4::rotation(*this); +template Matrix4x4 Quaternion::toMatrix() const { + return Matrix4x4::rotation(*this); } /** * @brief Transform class for handling position, rotation, and scale - * + * * @tparam T Numeric type (float, double, etc.) */ -template -class Transform { -public: - using Vec3 = Vector3; - using Quat = Quaternion; - using Mat4 = Matrix4x4; - - Transform() - : position_(Vec3(0, 0, 0)), - rotation_(Quat()), - scale_(Vec3(1, 1, 1)), - dirty_(true) {} - - // Get components - const Vec3& getPosition() const { return position_; } - const Quat& getRotation() const { return rotation_; } - const Vec3& getScale() const { return scale_; } - - // Set components - void setPosition(const Vec3& position) { - position_ = position; - dirty_ = true; - } - - void setRotation(const Quat& rotation) { - rotation_ = rotation; - dirty_ = true; - } - - void setScale(const Vec3& scale) { - scale_ = scale; - dirty_ = true; - } - - // Set from euler angles (in radians) - void setRotationEuler(T pitch, T yaw, T roll) { - rotation_ = Quat::fromEulerAngles(pitch, yaw, roll); - dirty_ = true; - } - - // Set from axis angle - void setRotationAxisAngle(const Vec3& axis, T angle) { - rotation_ = Quat::fromAxisAngle(axis, angle); - dirty_ = true; - } - - // Get the transformation matrix - const Mat4& getMatrix() const { - if (dirty_) { - updateMatrix(); - } - return matrix_; - } - - // Transform a point from local to world space - template - Vector3 transformPoint(const Vector3& point) const { - return getMatrix().template transformPoint(point); - } - - // Transform a direction from local to world space - template - Vector3 transformDirection(const Vector3& direction) const { - return getMatrix().template transformDirection(direction); - } - - // Combine two transforms - Transform operator*(const Transform& other) const { - Transform result; - result.matrix_ = getMatrix() * other.getMatrix(); - result.dirty_ = false; - - // Extract position from column 3 of the combined matrix - const auto& m = result.matrix_; - result.position_ = Vec3(m(0, 3), m(1, 3), m(2, 3)); - - // Extract scale as column vector lengths - T sx = std::sqrt(m(0,0)*m(0,0) + m(1,0)*m(1,0) + m(2,0)*m(2,0)); - T sy = std::sqrt(m(0,1)*m(0,1) + m(1,1)*m(1,1) + m(2,1)*m(2,1)); - T sz = std::sqrt(m(0,2)*m(0,2) + m(1,2)*m(1,2) + m(2,2)*m(2,2)); - result.scale_ = Vec3(sx, sy, sz); - - // Extract rotation: divide upper-left 3x3 by scale, then convert to quaternion - if (sx > 0 && sy > 0 && sz > 0) { - T r00 = m(0,0)/sx, r01 = m(0,1)/sy, r02 = m(0,2)/sz; - T r10 = m(1,0)/sx, r11 = m(1,1)/sy, r12 = m(1,2)/sz; - T r20 = m(2,0)/sx, r21 = m(2,1)/sy, r22 = m(2,2)/sz; - - T trace = r00 + r11 + r22; - if (trace > 0) { - T s = T(0.5) / std::sqrt(trace + T(1)); - result.rotation_ = Quat( - (r21 - r12) * s, - (r02 - r20) * s, - (r10 - r01) * s, - T(0.25) / s - ); - } else if (r00 > r11 && r00 > r22) { - T s = T(2) * std::sqrt(T(1) + r00 - r11 - r22); - result.rotation_ = Quat( - T(0.25) * s, - (r01 + r10) / s, - (r02 + r20) / s, - (r21 - r12) / s - ); - } else if (r11 > r22) { - T s = T(2) * std::sqrt(T(1) + r11 - r00 - r22); - result.rotation_ = Quat( - (r01 + r10) / s, - T(0.25) * s, - (r12 + r21) / s, - (r02 - r20) / s - ); - } else { - T s = T(2) * std::sqrt(T(1) + r22 - r00 - r11); - result.rotation_ = Quat( - (r02 + r20) / s, - (r12 + r21) / s, - T(0.25) * s, - (r10 - r01) / s - ); - } - } - - return result; - } - - // Combine this transform with another, resulting in a new transform - Transform combine(const Transform& other) const { - Transform result; - - // Simple combination for position, scale, and rotation - // Element-wise multiplication for scale - Vec3 scaledPos = Vec3( - other.position_.x * this->scale_.x, - other.position_.y * this->scale_.y, - other.position_.z * this->scale_.z - ); - result.position_ = this->position_ + this->rotation_.rotateVector(scaledPos); - - // Element-wise multiplication for scale - result.scale_ = Vec3( - this->scale_.x * other.scale_.x, - this->scale_.y * other.scale_.y, - this->scale_.z * other.scale_.z - ); - - result.rotation_ = this->rotation_ * other.rotation_; - result.dirty_ = true; - - return result; - } - -private: - Vec3 position_; - Quat rotation_; - Vec3 scale_; - mutable Mat4 matrix_; - mutable bool dirty_; - - void updateMatrix() const { - // Build the transformation matrix from components - Mat4 translationMatrix = Mat4::translation(position_); - Mat4 rotationMatrix = Mat4::rotation(rotation_); - Mat4 scaleMatrix = Mat4::scaling(scale_); - - // Combine the transformations - matrix_ = translationMatrix * rotationMatrix * scaleMatrix; - dirty_ = false; - } +template class Transform { + public: + using Vec3 = Vector3; + using Quat = Quaternion; + using Mat4 = Matrix4x4; + + Transform() : position_(Vec3(0, 0, 0)), rotation_(Quat()), scale_(Vec3(1, 1, 1)), dirty_(true) {} + + // Get components + const Vec3& getPosition() const { return position_; } + const Quat& getRotation() const { return rotation_; } + const Vec3& getScale() const { return scale_; } + + // Set components + void setPosition(const Vec3& position) { + position_ = position; + dirty_ = true; + } + + void setRotation(const Quat& rotation) { + rotation_ = rotation; + dirty_ = true; + } + + void setScale(const Vec3& scale) { + scale_ = scale; + dirty_ = true; + } + + // Set from euler angles (in radians) + void setRotationEuler(T pitch, T yaw, T roll) { + rotation_ = Quat::fromEulerAngles(pitch, yaw, roll); + dirty_ = true; + } + + // Set from axis angle + void setRotationAxisAngle(const Vec3& axis, T angle) { + rotation_ = Quat::fromAxisAngle(axis, angle); + dirty_ = true; + } + + // Get the transformation matrix + const Mat4& getMatrix() const { + if (dirty_) { + updateMatrix(); + } + return matrix_; + } + + // Transform a point from local to world space + template Vector3 transformPoint(const Vector3& point) const { + return getMatrix().template transformPoint(point); + } + + // Transform a direction from local to world space + template + Vector3 transformDirection(const Vector3& direction) const { + return getMatrix().template transformDirection(direction); + } + + // Combine two transforms + Transform operator*(const Transform& other) const { + Transform result; + result.matrix_ = getMatrix() * other.getMatrix(); + result.dirty_ = false; + + // Extract position from column 3 of the combined matrix + const auto& m = result.matrix_; + result.position_ = Vec3(m(0, 3), m(1, 3), m(2, 3)); + + // Extract scale as column vector lengths + T sx = std::sqrt(m(0, 0) * m(0, 0) + m(1, 0) * m(1, 0) + m(2, 0) * m(2, 0)); + T sy = std::sqrt(m(0, 1) * m(0, 1) + m(1, 1) * m(1, 1) + m(2, 1) * m(2, 1)); + T sz = std::sqrt(m(0, 2) * m(0, 2) + m(1, 2) * m(1, 2) + m(2, 2) * m(2, 2)); + result.scale_ = Vec3(sx, sy, sz); + + // Extract rotation: divide upper-left 3x3 by scale, then convert to quaternion + if (sx > 0 && sy > 0 && sz > 0) { + T r00 = m(0, 0) / sx, r01 = m(0, 1) / sy, r02 = m(0, 2) / sz; + T r10 = m(1, 0) / sx, r11 = m(1, 1) / sy, r12 = m(1, 2) / sz; + T r20 = m(2, 0) / sx, r21 = m(2, 1) / sy, r22 = m(2, 2) / sz; + + T trace = r00 + r11 + r22; + if (trace > 0) { + T s = T(0.5) / std::sqrt(trace + T(1)); + result.rotation_ = Quat((r21 - r12) * s, (r02 - r20) * s, (r10 - r01) * s, T(0.25) / s); + } else if (r00 > r11 && r00 > r22) { + T s = T(2) * std::sqrt(T(1) + r00 - r11 - r22); + result.rotation_ = Quat(T(0.25) * s, (r01 + r10) / s, (r02 + r20) / s, (r21 - r12) / s); + } else if (r11 > r22) { + T s = T(2) * std::sqrt(T(1) + r11 - r00 - r22); + result.rotation_ = Quat((r01 + r10) / s, T(0.25) * s, (r12 + r21) / s, (r02 - r20) / s); + } else { + T s = T(2) * std::sqrt(T(1) + r22 - r00 - r11); + result.rotation_ = Quat((r02 + r20) / s, (r12 + r21) / s, T(0.25) * s, (r10 - r01) / s); + } + } + + return result; + } + + // Combine this transform with another, resulting in a new transform + Transform combine(const Transform& other) const { + Transform result; + + // Simple combination for position, scale, and rotation + // Element-wise multiplication for scale + Vec3 scaledPos = Vec3(other.position_.x * this->scale_.x, other.position_.y * this->scale_.y, + other.position_.z * this->scale_.z); + result.position_ = this->position_ + this->rotation_.rotateVector(scaledPos); + + // Element-wise multiplication for scale + result.scale_ = + Vec3(this->scale_.x * other.scale_.x, this->scale_.y * other.scale_.y, this->scale_.z * other.scale_.z); + + result.rotation_ = this->rotation_ * other.rotation_; + result.dirty_ = true; + + return result; + } + + private: + Vec3 position_; + Quat rotation_; + Vec3 scale_; + mutable Mat4 matrix_; + mutable bool dirty_; + + void updateMatrix() const { + // Build the transformation matrix from components + Mat4 translationMatrix = Mat4::translation(position_); + Mat4 rotationMatrix = Mat4::rotation(rotation_); + Mat4 scaleMatrix = Mat4::scaling(scale_); + + // Combine the transformations + matrix_ = translationMatrix * rotationMatrix * scaleMatrix; + dirty_ = false; + } }; -} // namespace fabric \ No newline at end of file +} // namespace fabric diff --git a/include/fabric/core/StateMachine.hh b/include/fabric/core/StateMachine.hh index 2547347a..17d88aa7 100644 --- a/include/fabric/core/StateMachine.hh +++ b/include/fabric/core/StateMachine.hh @@ -15,165 +15,159 @@ namespace fabric { // Generic parameterizable state machine with configurable transitions and hooks. // Thread-safe via mutex. Self-transitions are no-ops. -template -class StateMachine { -public: - using Hook = std::function; - using ToStringFn = std::function; - - StateMachine(StateEnum initialState, ToStringFn toStringFn) - : currentState_(initialState), toStringFn_(std::move(toStringFn)) {} - - void addTransition(StateEnum from, StateEnum to) { - transitions_.insert({from, to}); - } - - void setState(StateEnum state) { - std::vector stateHooksToInvoke; - std::vector transHooksToInvoke; - StateEnum oldState; - - { - std::lock_guard lock(stateMutex_); - - if (currentState_ == state) { - return; - } - - if (transitions_.count({currentState_, state}) == 0) { - throwError("Invalid state transition from " + - toStringFn_(currentState_) + " to " + toStringFn_(state)); - } - - oldState = currentState_; - currentState_ = state; - } +template class StateMachine { + public: + using Hook = std::function; + using ToStringFn = std::function; + + StateMachine(StateEnum initialState, ToStringFn toStringFn) + : currentState_(initialState), toStringFn_(std::move(toStringFn)) {} + + void addTransition(StateEnum from, StateEnum to) { transitions_.insert({from, to}); } + + void setState(StateEnum state) { + std::vector stateHooksToInvoke; + std::vector transHooksToInvoke; + StateEnum oldState; + + { + std::lock_guard lock(stateMutex_); - FABRIC_LOG_DEBUG("State transition: {} -> {}", - toStringFn_(oldState), toStringFn_(state)); + if (currentState_ == state) { + return; + } - { - std::lock_guard lock(hooksMutex_); + if (transitions_.count({currentState_, state}) == 0) { + throwError("Invalid state transition from " + toStringFn_(currentState_) + " to " + toStringFn_(state)); + } - auto it = stateHooks_.find(state); - if (it != stateHooks_.end()) { - for (const auto& entry : it->second) { - stateHooksToInvoke.push_back(entry.hook); + oldState = currentState_; + currentState_ = state; } - } - auto key = transitionKey(oldState, state); - auto transIt = transitionHooks_.find(key); - if (transIt != transitionHooks_.end()) { - for (const auto& entry : transIt->second) { - transHooksToInvoke.push_back(entry.hook); + FABRIC_LOG_DEBUG("State transition: {} -> {}", toStringFn_(oldState), toStringFn_(state)); + + { + std::lock_guard lock(hooksMutex_); + + auto it = stateHooks_.find(state); + if (it != stateHooks_.end()) { + for (const auto& entry : it->second) { + stateHooksToInvoke.push_back(entry.hook); + } + } + + auto key = transitionKey(oldState, state); + auto transIt = transitionHooks_.find(key); + if (transIt != transitionHooks_.end()) { + for (const auto& entry : transIt->second) { + transHooksToInvoke.push_back(entry.hook); + } + } + } + + for (const auto& hook : stateHooksToInvoke) { + try { + hook(); + } catch (const std::exception& e) { + FABRIC_LOG_ERROR("Exception in state hook: {}", e.what()); + } catch (...) { + FABRIC_LOG_ERROR("Unknown exception in state hook"); + } } - } - } - for (const auto& hook : stateHooksToInvoke) { - try { - hook(); - } catch (const std::exception& e) { - FABRIC_LOG_ERROR("Exception in state hook: {}", e.what()); - } catch (...) { - FABRIC_LOG_ERROR("Unknown exception in state hook"); - } + for (const auto& hook : transHooksToInvoke) { + try { + hook(); + } catch (const std::exception& e) { + FABRIC_LOG_ERROR("Exception in transition hook: {}", e.what()); + } catch (...) { + FABRIC_LOG_ERROR("Unknown exception in transition hook"); + } + } } - for (const auto& hook : transHooksToInvoke) { - try { - hook(); - } catch (const std::exception& e) { - FABRIC_LOG_ERROR("Exception in transition hook: {}", e.what()); - } catch (...) { - FABRIC_LOG_ERROR("Unknown exception in transition hook"); - } + StateEnum getState() const { + std::lock_guard lock(stateMutex_); + return currentState_; } - } - StateEnum getState() const { - std::lock_guard lock(stateMutex_); - return currentState_; - } + bool isValidTransition(StateEnum from, StateEnum to) const { + if (from == to) + return true; + return transitions_.count({from, to}) > 0; + } - bool isValidTransition(StateEnum from, StateEnum to) const { - if (from == to) return true; - return transitions_.count({from, to}) > 0; - } + std::string addHook(StateEnum state, const Hook& hook) { + if (!hook) { + throwError("State hook cannot be null"); + } - std::string addHook(StateEnum state, const Hook& hook) { - if (!hook) { - throwError("State hook cannot be null"); + std::lock_guard lock(hooksMutex_); + auto id = Utils::generateUniqueId("hook_"); + stateHooks_[state].push_back(HookEntry{id, hook}); + FABRIC_LOG_DEBUG("Added state hook for '{}' with ID '{}'", toStringFn_(state), id); + return id; } - std::lock_guard lock(hooksMutex_); - auto id = Utils::generateUniqueId("hook_"); - stateHooks_[state].push_back(HookEntry{id, hook}); - FABRIC_LOG_DEBUG("Added state hook for '{}' with ID '{}'", - toStringFn_(state), id); - return id; - } - - std::string addTransitionHook(StateEnum from, StateEnum to, const Hook& hook) { - if (!hook) { - throwError("Transition hook cannot be null"); - } + std::string addTransitionHook(StateEnum from, StateEnum to, const Hook& hook) { + if (!hook) { + throwError("Transition hook cannot be null"); + } - std::lock_guard lock(hooksMutex_); - auto id = Utils::generateUniqueId("transition_"); - auto key = transitionKey(from, to); - transitionHooks_[key].push_back(HookEntry{id, hook}); - FABRIC_LOG_DEBUG("Added transition hook from '{}' to '{}' with ID '{}'", - toStringFn_(from), toStringFn_(to), id); - return id; - } - - bool removeHook(const std::string& hookId) { - std::lock_guard lock(hooksMutex_); - - for (auto& [state, hooks] : stateHooks_) { - auto it = std::find_if(hooks.begin(), hooks.end(), - [&hookId](const HookEntry& e) { return e.id == hookId; }); - if (it != hooks.end()) { - hooks.erase(it); - FABRIC_LOG_DEBUG("Removed hook with ID '{}'", hookId); - return true; - } + std::lock_guard lock(hooksMutex_); + auto id = Utils::generateUniqueId("transition_"); + auto key = transitionKey(from, to); + transitionHooks_[key].push_back(HookEntry{id, hook}); + FABRIC_LOG_DEBUG("Added transition hook from '{}' to '{}' with ID '{}'", toStringFn_(from), toStringFn_(to), + id); + return id; } - for (auto& [key, hooks] : transitionHooks_) { - auto it = std::find_if(hooks.begin(), hooks.end(), - [&hookId](const HookEntry& e) { return e.id == hookId; }); - if (it != hooks.end()) { - hooks.erase(it); - FABRIC_LOG_DEBUG("Removed transition hook with ID '{}'", hookId); - return true; - } - } + bool removeHook(const std::string& hookId) { + std::lock_guard lock(hooksMutex_); + + for (auto& [state, hooks] : stateHooks_) { + auto it = + std::find_if(hooks.begin(), hooks.end(), [&hookId](const HookEntry& e) { return e.id == hookId; }); + if (it != hooks.end()) { + hooks.erase(it); + FABRIC_LOG_DEBUG("Removed hook with ID '{}'", hookId); + return true; + } + } - return false; - } + for (auto& [key, hooks] : transitionHooks_) { + auto it = + std::find_if(hooks.begin(), hooks.end(), [&hookId](const HookEntry& e) { return e.id == hookId; }); + if (it != hooks.end()) { + hooks.erase(it); + FABRIC_LOG_DEBUG("Removed transition hook with ID '{}'", hookId); + return true; + } + } + + return false; + } -private: - struct HookEntry { - std::string id; - Hook hook; - }; + private: + struct HookEntry { + std::string id; + Hook hook; + }; - static std::string transitionKey(StateEnum from, StateEnum to) { - return std::to_string(static_cast(from)) + ":" + - std::to_string(static_cast(to)); - } + static std::string transitionKey(StateEnum from, StateEnum to) { + return std::to_string(static_cast(from)) + ":" + std::to_string(static_cast(to)); + } - mutable std::mutex stateMutex_; - StateEnum currentState_; - ToStringFn toStringFn_; - std::set> transitions_; + mutable std::mutex stateMutex_; + StateEnum currentState_; + ToStringFn toStringFn_; + std::set> transitions_; - mutable std::mutex hooksMutex_; - std::unordered_map> stateHooks_; - std::unordered_map> transitionHooks_; + mutable std::mutex hooksMutex_; + std::unordered_map> stateHooks_; + std::unordered_map> transitionHooks_; }; } // namespace fabric diff --git a/include/fabric/core/Temporal.hh b/include/fabric/core/Temporal.hh index 40846508..3075ed22 100644 --- a/include/fabric/core/Temporal.hh +++ b/include/fabric/core/Temporal.hh @@ -1,74 +1,70 @@ #ifndef FABRIC_CORE_TEMPORAL_HH #define FABRIC_CORE_TEMPORAL_HH -#include +#include +#include // For memcpy #include -#include -#include #include -#include +#include #include -#include #include -#include // For memcpy +#include +#include +#include namespace fabric { /** * @brief TimeState captures the state of a timeline at a specific moment - * + * * TimeState is used for creating snapshots of system state that can be * restored later, enabling time manipulation and debugging. */ class TimeState { -public: + public: using EntityID = std::string; using Timestamp = std::chrono::steady_clock::time_point; - + TimeState(); explicit TimeState(double timestamp); - + /** Add an entity's state to this time state */ - template - void setEntityState(const EntityID& entityId, const StateType& state) { + template void setEntityState(const EntityID& entityId, const StateType& state) { // Serialize the state to bytes entityStates_[entityId] = serialize(state); } - + /** Retrieve an entity's state from this time state */ - template - std::optional getEntityState(const EntityID& entityId) const { + template std::optional getEntityState(const EntityID& entityId) const { auto it = entityStates_.find(entityId); if (it == entityStates_.end()) { return std::nullopt; } - + return deserialize(it->second); } - + /** Compare this time state with another and return differences */ std::unordered_map diff(const TimeState& other) const; - + /** Get the timestamp of this state */ double getTimestamp() const; - + /** Clone this time state */ std::unique_ptr clone() const; - -private: + + private: double timestamp_; std::unordered_map> entityStates_; - + // Helper functions for serialization - template - static std::vector serialize(const T& data) { + template static std::vector serialize(const T& data) { std::vector buffer(sizeof(T)); std::memcpy(buffer.data(), &data, sizeof(T)); return buffer; } - - template - static T deserialize(const std::vector& data) { + + template static T deserialize(const std::vector& data) { T result; if (data.size() >= sizeof(T)) { std::memcpy(&result, data.data(), sizeof(T)); @@ -79,21 +75,21 @@ private: /** * @brief TimeRegion represents a region of space that can have its own time flow - * + * * Different regions can progress at different rates, allowing for localized * time manipulation. */ class TimeRegion { -public: + public: TimeRegion(); explicit TimeRegion(double timeScale); - + /** Update this region with the given global delta time */ void update(double worldDeltaTime); - + /** Get the time scale of this region */ double getTimeScale() const; - + /** Set the time scale of this region */ void setTimeScale(double scale); @@ -103,7 +99,7 @@ public: /** Restore local time from a snapshot */ void restoreSnapshot(const TimeState& state); -private: + private: double timeScale_; double localTime_; }; @@ -112,15 +108,15 @@ private: * @brief TimeBehavior is an interface for objects that need time-based updates */ class TimeBehavior { -public: + public: virtual ~TimeBehavior() = default; - + /** Update this behavior with the given delta time */ virtual void onTimeUpdate(double deltaTime) = 0; - + /** Create a state snapshot of this behavior */ virtual std::vector createSnapshot() const = 0; - + /** Restore from a state snapshot */ virtual void restoreSnapshot(const std::vector& data) = 0; }; @@ -129,58 +125,58 @@ public: * @brief Timeline manages time flow and provides time manipulation capabilities */ class Timeline { -public: + public: Timeline(); - + /** Update the timeline with the given real-world delta time */ void update(double deltaTime); - + /** Create a new time region with the given scale */ TimeRegion* createRegion(double timeScale = 1.0); - + /** Remove a time region */ void removeRegion(TimeRegion* region); - + /** Create a snapshot of the entire timeline */ TimeState createSnapshot() const; - + /** Restore a previously created snapshot */ void restoreSnapshot(const TimeState& state); - + /** Get the current timeline time */ double getCurrentTime() const; - + /** Set the global time scale factor */ void setGlobalTimeScale(double scale); - + /** Get the global time scale factor */ double getGlobalTimeScale() const; - + /** Pause the timeline */ void pause(); - + /** Resume the timeline */ void resume(); - + /** Check if the timeline is paused */ bool isPaused() const; - + /** Enable/disable automatic snapshots */ void setAutomaticSnapshots(bool enable, double interval = 1.0); - + /** Access the history of automatic snapshots */ const std::deque& getHistory() const; - + /** Clear snapshot history */ void clearHistory(); - + /** Jump to a specific point in the snapshot history */ bool jumpToSnapshot(size_t index); /** Create a prediction of future state */ TimeState predictFutureState(double secondsAhead) const; -private: + private: /** Restore snapshot without acquiring mutex. Caller must hold mutex_. */ void restoreSnapshotLocked(const TimeState& state); double currentTime_; @@ -202,19 +198,15 @@ private: /** * @brief Utility for time interpolation between states */ -template -class Interpolator { -public: +template class Interpolator { + public: static T lerp(const T& a, const T& b, double t); }; // Specialized interpolators for common types -template <> -class Interpolator { -public: - static double lerp(const double& a, const double& b, double t) { - return a + (b - a) * t; - } +template <> class Interpolator { + public: + static double lerp(const double& a, const double& b, double t) { return a + (b - a) * t; } }; // Helper functions @@ -222,31 +214,25 @@ public: * @brief Creates a time behavior from a lambda function */ template -std::unique_ptr makeTimeBehavior( - std::function updateFunc, - std::function getStateFunc, - std::function setStateFunc -) { +std::unique_ptr makeTimeBehavior(std::function updateFunc, + std::function getStateFunc, + std::function setStateFunc) { // Create a lambda-based TimeBehavior implementation class LambdaTimeBehavior : public TimeBehavior { - public: - LambdaTimeBehavior( - std::function updateFunc, - std::function getStateFunc, - std::function setStateFunc - ) : updateFunc_(updateFunc), getStateFunc_(getStateFunc), setStateFunc_(setStateFunc) {} - - void onTimeUpdate(double deltaTime) override { - updateFunc_(deltaTime); - } - + public: + LambdaTimeBehavior(std::function updateFunc, std::function getStateFunc, + std::function setStateFunc) + : updateFunc_(updateFunc), getStateFunc_(getStateFunc), setStateFunc_(setStateFunc) {} + + void onTimeUpdate(double deltaTime) override { updateFunc_(deltaTime); } + std::vector createSnapshot() const override { StateType state = getStateFunc_(); std::vector data(sizeof(StateType)); std::memcpy(data.data(), &state, sizeof(StateType)); return data; } - + void restoreSnapshot(const std::vector& data) override { if (data.size() >= sizeof(StateType)) { StateType state; @@ -254,16 +240,16 @@ std::unique_ptr makeTimeBehavior( setStateFunc_(state); } } - - private: + + private: std::function updateFunc_; std::function getStateFunc_; std::function setStateFunc_; }; - + return std::make_unique(updateFunc, getStateFunc, setStateFunc); }; } // namespace fabric -#endif // FABRIC_CORE_TEMPORAL_HH \ No newline at end of file +#endif // FABRIC_CORE_TEMPORAL_HH diff --git a/include/fabric/core/Types.hh b/include/fabric/core/Types.hh index 1ba2ffbb..17aeb0a0 100644 --- a/include/fabric/core/Types.hh +++ b/include/fabric/core/Types.hh @@ -1,15 +1,15 @@ #pragma once #include -#include -#include #include -#include -#include #include +#include #include +#include #include #include +#include +#include namespace fabric { @@ -18,11 +18,9 @@ using BinaryData = std::vector; using Variant = std::variant>; -template -using StringMap = std::map; +template using StringMap = std::map; -template -using Optional = std::optional; +template using Optional = std::optional; // Forward declarations for use in type aliases struct Token; @@ -53,5 +51,4 @@ using TokenTypeOptionsMap = StringMap; */ using StringStringMap = StringMap; - -} // namespace fabric \ No newline at end of file +} // namespace fabric diff --git a/include/fabric/core/VoxelMesher.hh b/include/fabric/core/VoxelMesher.hh index b0cb130e..a40fea4d 100644 --- a/include/fabric/core/VoxelMesher.hh +++ b/include/fabric/core/VoxelMesher.hh @@ -2,17 +2,17 @@ #include "fabric/core/ChunkedGrid.hh" #include "fabric/core/Spatial.hh" -#include -#include #include +#include #include +#include namespace fabric { struct VoxelVertex { - float px, py, pz; // position - float nx, ny, nz; // normal - float r, g, b, a; // color (from essence) + float px, py, pz; // position + float nx, ny, nz; // normal + float r, g, b, a; // color (from essence) }; struct ChunkMeshData { @@ -28,22 +28,17 @@ struct ChunkMesh { }; class VoxelMesher { -public: + public: static bgfx::VertexLayout getVertexLayout(); // Generate raw mesh data (no bgfx, testable without GPU) - static ChunkMeshData meshChunkData( - int cx, int cy, int cz, - const ChunkedGrid& density, - const ChunkedGrid>& essence, - float threshold = 0.5f); + static ChunkMeshData meshChunkData(int cx, int cy, int cz, const ChunkedGrid& density, + const ChunkedGrid>& essence, + float threshold = 0.5f); // Generate bgfx mesh (requires bgfx initialized) - static ChunkMesh meshChunk( - int cx, int cy, int cz, - const ChunkedGrid& density, - const ChunkedGrid>& essence, - float threshold = 0.5f); + static ChunkMesh meshChunk(int cx, int cy, int cz, const ChunkedGrid& density, + const ChunkedGrid>& essence, float threshold = 0.5f); static void destroyMesh(ChunkMesh& mesh); }; diff --git a/include/fabric/parser/ArgumentParser.hh b/include/fabric/parser/ArgumentParser.hh index 15830bcd..4a345882 100644 --- a/include/fabric/parser/ArgumentParser.hh +++ b/include/fabric/parser/ArgumentParser.hh @@ -1,60 +1,56 @@ #pragma once -#include "fabric/parser/SyntaxTree.hh" #include "fabric/core/Types.hh" +#include "fabric/parser/SyntaxTree.hh" #include #include namespace fabric { // ArgumentParser class ArgumentParser { -public: - ArgumentParser() = default; + public: + ArgumentParser() = default; - std::string getErrorMsg() const { return errorMsg; } - bool isValid() const { return valid; } + const std::string& getErrorMsg() const { return errorMsg; } + bool isValid() const { return valid; } - const TokenMap &getArguments() const; - const OptionalToken getArgument(const std::string &name) const; + const TokenMap& getArguments() const; + const OptionalToken getArgument(const std::string& name) const; - // Add a command-line argument definition - void addArgument(const std::string &name, const std::string &description, - bool required = false); + // Add a command-line argument definition + void addArgument(const std::string& name, const std::string& description, bool required = false); - // Check if an argument was provided in the command line - bool hasArgument(const std::string &name) const; + // Check if an argument was provided in the command line + bool hasArgument(const std::string& name) const; - void parse(int argc, char *argv[]); - void parse(const std::string &args); + void parse(int argc, char* argv[]); + void parse(const std::string& args); - bool validateArgs(const TokenTypeOptionsMap &options); + bool validateArgs(const TokenTypeOptionsMap& options); -private: - TokenMap arguments; - TokenTypeOptionsMap availableArgs; - StringStringMap argumentDescriptions; + private: + TokenMap arguments; + TokenTypeOptionsMap availableArgs; + StringStringMap argumentDescriptions; - std::string errorMsg; - bool valid = true; + std::string errorMsg; + bool valid = true; - // Allow ArgumentParserBuilder to access private members - friend class ArgumentParserBuilder; + // Allow ArgumentParserBuilder to access private members + friend class ArgumentParserBuilder; }; // ArgumentParserBuilder // // Builder pattern for ArgumentParser class ArgumentParserBuilder { -public: - const TokenTypeOptionsMap &getOptions() const { - return options; - } + public: + const TokenTypeOptionsMap& getOptions() const { return options; } - ArgumentParserBuilder &addOption(const std::string &name, TokenType type, - bool optional = false); - ArgumentParser build() const; + ArgumentParserBuilder& addOption(const std::string& name, TokenType type, bool optional = false); + ArgumentParser build() const; -private: - TokenTypeOptionsMap options; + private: + TokenTypeOptionsMap options; }; } // namespace fabric diff --git a/include/fabric/parser/SyntaxTree.hh b/include/fabric/parser/SyntaxTree.hh index 3b2cfe24..63417706 100644 --- a/include/fabric/parser/SyntaxTree.hh +++ b/include/fabric/parser/SyntaxTree.hh @@ -8,22 +8,22 @@ namespace fabric { // Represents a node in the abstract syntax tree class ASTNode { -public: - ASTNode(const Token &token); + public: + ASTNode(const Token& token); - // Getters - const Token &getToken() const; - const std::vector> &getChildren() const; + // Getters + const Token& getToken() const; + const std::vector>& getChildren() const; - void addChild(std::shared_ptr child); + void addChild(std::shared_ptr child); -private: - Token token; - std::vector> children; + private: + Token token; + std::vector> children; }; // Helper functions -TokenType determineTokenType(const std::string &token); +TokenType determineTokenType(const std::string& token); -Variant parseValue(const std::string &token, TokenType type); +Variant parseValue(const std::string& token, TokenType type); } // namespace fabric diff --git a/include/fabric/parser/Token.hh b/include/fabric/parser/Token.hh index 25574e33..94bc84ec 100644 --- a/include/fabric/parser/Token.hh +++ b/include/fabric/parser/Token.hh @@ -6,225 +6,225 @@ namespace fabric { // Enum for token types with payloads for literals enum class TokenType { - // CLI - CLIFlag, // --flag (valueless flag) - CLIOption, // --option=value or --option value - CLIPositional, // positional argument - CLICommand, // command in command-based CLIs - - // Control Flow - KeywordIf, // if - KeywordElse, // else - KeywordFor, // for - KeywordWhile, // while - KeywordReturn, // return - KeywordGoto, // goto - KeywordBreak, // break - KeywordContinue, // continue - KeywordSwitch, // switch - KeywordCase, // case - KeywordDefault, // default - KeywordDefer, // defer (Go-like) - - // Error Handling - KeywordTry, // try - KeywordCatch, // catch - KeywordThrow, // throw - KeywordFinally, // finally - KeywordRaise, // raise - KeywordAssert, // assert - - // Data Types - KeywordFunction, // func, def, fn, function - KeywordStruct, // struct - KeywordEnum, // enum - KeywordArray, // array - KeywordMap, // map, dict - KeywordSet, // set - KeywordTuple, // tuple - KeywordGeneric, // generic, template - KeywordWhere, // where (constraints) - - // Object-Oriented Inheritance - KeywordClass, // class - KeywordInterface, // interface - KeywordImplements, // implements - KeywordExtends, // extends - KeywordSelf, // self - KeywordSuper, // super - KeywordOverride, // override - KeywordAbstract, // abstract - KeywordVirtual, // virtual - KeywordDelegate, // delegate - KeywordEvent, // event - - // Modules - KeywordImport, // import, include, use - KeywordPackage, // package, module, namespace - KeywordExport, // export - KeywordFrom, // from - - // Declarations - KeywordConst, // const - KeywordLet, // let - KeywordVar, // var - KeywordType, // type - KeywordMut, // mut - KeywordUnsafe, // unsafe - KeywordStatic, // static - - // Memory Management - KeywordNew, // new - KeywordDelete, // delete - KeywordAlloc, // alloc - KeywordFree, // free - KeywordMove, // move - KeywordBorrow, // borrow (Rust-like) - - // Access Modifiers - KeywordPublic, // pub - KeywordPrivate, // priv - KeywordProtected, // prot - KeywordInternal, // int - KeywordFinal, // final - - // Boolean Operators - KeywordAs, // as - KeywordIs, // is - KeywordIn, // in - KeywordNot, // not - KeywordAnd, // and - KeywordOr, // or - - // Functional Programming - KeywordLambda, // lambda, => - KeywordClosure, // closure - KeywordCurry, // curry - KeywordPipe, // pipe, |> - KeywordCompose, // compose - - // Concurrency - KeywordThread, // thread - KeywordAtomic, // atomic - KeywordSync, // sync - KeywordLock, // lock - KeywordMutex, // mutex - - // Async - KeywordYield, // yield - KeywordAsync, // async - KeywordAwait, // await - - // Operators - OperatorPlus, // + - OperatorMinus, // - - OperatorMultiply, // * - OperatorDivide, // / - OperatorModulo, // % - OperatorAssign, // = - OperatorEqual, // == - OperatorNotEqual, // !=, <> - OperatorLessThan, // < - OperatorGreaterThan, // > - OperatorLessEqual, // <= - OperatorGreaterEqual, // >= - OperatorPower, // ** - OperatorBitwiseAnd, // & - OperatorBitwiseOr, // | - OperatorBitwiseXor, // ^ - OperatorBitwiseNot, // ~ - OperatorShiftLeft, // << - OperatorShiftRight, // >> - OperatorAssignAdd, // += - OperatorAssignSubtract, // -= - OperatorAssignMultiply, // *= - OperatorAssignDivide, // /= - OperatorAssignModulo, // %= - OperatorAssignBitwiseAnd, // &= - OperatorAssignBitwiseOr, // |= - OperatorAssignBitwiseXor, // ^= - OperatorAssignBitwiseNot, // ~= - OperatorAssignShiftLeft, // <<= - OperatorAssignShiftRight, // >>= - OperatorAssignPower, // **= - OperatorIncrement, // ++ - OperatorDecrement, // -- - OperatorNullCoalesce, // ?? - OperatorOptionalChaining, // ?. - OperatorSpread, // ... - OperatorRangeInclusive, // ..= - OperatorRangeExclusive, // .. - OperatorPipeline, // |> - - // Delimiters - DelimiterSemicolon, // ; - DelimiterComma, // , - DelimiterDot, // . - DelimiterColon, // : - DelimiterOpenParen, // ( - DelimiterCloseParen, // ) - DelimiterOpenBrace, // { - DelimiterCloseBrace, // } - DelimiterOpenBracket, // [ - DelimiterCloseBracket, // ] - DelimiterDoubleColon, // :: - DelimiterArrow, // -> - DelimiterFatArrow, // => - DelimiterBacktick, // ` - - // Literals - LiteralNull, // null, nil, None - LiteralNumber, // num(42) - LiteralString, // str("hello") - LiteralBoolean, // bool(true) - LiteralFloat, // float(3.14) - LiteralChar, // char('a') - LiteralRegex, // regex(/pattern/) - LiteralDate, // date(2023-01-01) - LiteralTemplate, // template(`string ${expr}`) - LiteralBinary, // binary(0b101) - LiteralHex, // hex(0xFF) - LiteralOctal, // octal(0o77) - LiteralBigInt, // bigint(1234567890123456789n) - - // Preprocessor - PreprocessorInclude, // #include - PreprocessorDefine, // #define - PreprocessorIf, // #if - PreprocessorElse, // #else - PreprocessorEndif, // #endif - - // Meta-programming - MetaQuote, // quote - MetaUnquote, // unquote - MetaSplice, // splice - MetaMacro, // macro - - // Identifiers - Identifier, - - // Comments - CommentLine, // //, # - CommentBlock, // /* */ - - // Whitespace - Whitespace, - Newline, - Tab, - CarriageReturn, - Space, - - // End of File - EndOfFile, + // CLI + CLIFlag, // --flag (valueless flag) + CLIOption, // --option=value or --option value + CLIPositional, // positional argument + CLICommand, // command in command-based CLIs + + // Control Flow + KeywordIf, // if + KeywordElse, // else + KeywordFor, // for + KeywordWhile, // while + KeywordReturn, // return + KeywordGoto, // goto + KeywordBreak, // break + KeywordContinue, // continue + KeywordSwitch, // switch + KeywordCase, // case + KeywordDefault, // default + KeywordDefer, // defer (Go-like) + + // Error Handling + KeywordTry, // try + KeywordCatch, // catch + KeywordThrow, // throw + KeywordFinally, // finally + KeywordRaise, // raise + KeywordAssert, // assert + + // Data Types + KeywordFunction, // func, def, fn, function + KeywordStruct, // struct + KeywordEnum, // enum + KeywordArray, // array + KeywordMap, // map, dict + KeywordSet, // set + KeywordTuple, // tuple + KeywordGeneric, // generic, template + KeywordWhere, // where (constraints) + + // Object-Oriented Inheritance + KeywordClass, // class + KeywordInterface, // interface + KeywordImplements, // implements + KeywordExtends, // extends + KeywordSelf, // self + KeywordSuper, // super + KeywordOverride, // override + KeywordAbstract, // abstract + KeywordVirtual, // virtual + KeywordDelegate, // delegate + KeywordEvent, // event + + // Modules + KeywordImport, // import, include, use + KeywordPackage, // package, module, namespace + KeywordExport, // export + KeywordFrom, // from + + // Declarations + KeywordConst, // const + KeywordLet, // let + KeywordVar, // var + KeywordType, // type + KeywordMut, // mut + KeywordUnsafe, // unsafe + KeywordStatic, // static + + // Memory Management + KeywordNew, // new + KeywordDelete, // delete + KeywordAlloc, // alloc + KeywordFree, // free + KeywordMove, // move + KeywordBorrow, // borrow (Rust-like) + + // Access Modifiers + KeywordPublic, // pub + KeywordPrivate, // priv + KeywordProtected, // prot + KeywordInternal, // int + KeywordFinal, // final + + // Boolean Operators + KeywordAs, // as + KeywordIs, // is + KeywordIn, // in + KeywordNot, // not + KeywordAnd, // and + KeywordOr, // or + + // Functional Programming + KeywordLambda, // lambda, => + KeywordClosure, // closure + KeywordCurry, // curry + KeywordPipe, // pipe, |> + KeywordCompose, // compose + + // Concurrency + KeywordThread, // thread + KeywordAtomic, // atomic + KeywordSync, // sync + KeywordLock, // lock + KeywordMutex, // mutex + + // Async + KeywordYield, // yield + KeywordAsync, // async + KeywordAwait, // await + + // Operators + OperatorPlus, // + + OperatorMinus, // - + OperatorMultiply, // * + OperatorDivide, // / + OperatorModulo, // % + OperatorAssign, // = + OperatorEqual, // == + OperatorNotEqual, // !=, <> + OperatorLessThan, // < + OperatorGreaterThan, // > + OperatorLessEqual, // <= + OperatorGreaterEqual, // >= + OperatorPower, // ** + OperatorBitwiseAnd, // & + OperatorBitwiseOr, // | + OperatorBitwiseXor, // ^ + OperatorBitwiseNot, // ~ + OperatorShiftLeft, // << + OperatorShiftRight, // >> + OperatorAssignAdd, // += + OperatorAssignSubtract, // -= + OperatorAssignMultiply, // *= + OperatorAssignDivide, // /= + OperatorAssignModulo, // %= + OperatorAssignBitwiseAnd, // &= + OperatorAssignBitwiseOr, // |= + OperatorAssignBitwiseXor, // ^= + OperatorAssignBitwiseNot, // ~= + OperatorAssignShiftLeft, // <<= + OperatorAssignShiftRight, // >>= + OperatorAssignPower, // **= + OperatorIncrement, // ++ + OperatorDecrement, // -- + OperatorNullCoalesce, // ?? + OperatorOptionalChaining, // ?. + OperatorSpread, // ... + OperatorRangeInclusive, // ..= + OperatorRangeExclusive, // .. + OperatorPipeline, // |> + + // Delimiters + DelimiterSemicolon, // ; + DelimiterComma, // , + DelimiterDot, // . + DelimiterColon, // : + DelimiterOpenParen, // ( + DelimiterCloseParen, // ) + DelimiterOpenBrace, // { + DelimiterCloseBrace, // } + DelimiterOpenBracket, // [ + DelimiterCloseBracket, // ] + DelimiterDoubleColon, // :: + DelimiterArrow, // -> + DelimiterFatArrow, // => + DelimiterBacktick, // ` + + // Literals + LiteralNull, // null, nil, None + LiteralNumber, // num(42) + LiteralString, // str("hello") + LiteralBoolean, // bool(true) + LiteralFloat, // float(3.14) + LiteralChar, // char('a') + LiteralRegex, // regex(/pattern/) + LiteralDate, // date(2023-01-01) + LiteralTemplate, // template(`string ${expr}`) + LiteralBinary, // binary(0b101) + LiteralHex, // hex(0xFF) + LiteralOctal, // octal(0o77) + LiteralBigInt, // bigint(1234567890123456789n) + + // Preprocessor + PreprocessorInclude, // #include + PreprocessorDefine, // #define + PreprocessorIf, // #if + PreprocessorElse, // #else + PreprocessorEndif, // #endif + + // Meta-programming + MetaQuote, // quote + MetaUnquote, // unquote + MetaSplice, // splice + MetaMacro, // macro + + // Identifiers + Identifier, + + // Comments + CommentLine, // //, # + CommentBlock, // /* */ + + // Whitespace + Whitespace, + Newline, + Tab, + CarriageReturn, + Space, + + // End of File + EndOfFile, }; // Token structure with type and value struct Token { - TokenType type; - Variant value; + TokenType type; + Variant value; - Token(TokenType type, Variant value = nullptr) : type(type), value(value) {} + Token(TokenType type, Variant value = nullptr) : type(type), value(value) {} - Token() : type(TokenType::EndOfFile), value(nullptr) {} + Token() : type(TokenType::EndOfFile), value(nullptr) {} }; } // namespace fabric diff --git a/include/fabric/ui/BgfxRenderInterface.hh b/include/fabric/ui/BgfxRenderInterface.hh index 3175d878..9bc03855 100644 --- a/include/fabric/ui/BgfxRenderInterface.hh +++ b/include/fabric/ui/BgfxRenderInterface.hh @@ -9,7 +9,7 @@ namespace fabric { class BgfxRenderInterface : public Rml::RenderInterface { -public: + public: BgfxRenderInterface(); ~BgfxRenderInterface() override; @@ -24,23 +24,17 @@ public: // -- RenderInterface required methods -- - Rml::CompiledGeometryHandle CompileGeometry( - Rml::Span vertices, - Rml::Span indices) override; + Rml::CompiledGeometryHandle CompileGeometry(Rml::Span vertices, + Rml::Span indices) override; - void RenderGeometry( - Rml::CompiledGeometryHandle geometry, - Rml::Vector2f translation, - Rml::TextureHandle texture) override; + void RenderGeometry(Rml::CompiledGeometryHandle geometry, Rml::Vector2f translation, + Rml::TextureHandle texture) override; void ReleaseGeometry(Rml::CompiledGeometryHandle geometry) override; - Rml::TextureHandle LoadTexture(Rml::Vector2i& dimensions, - const Rml::String& source) override; + Rml::TextureHandle LoadTexture(Rml::Vector2i& dimensions, const Rml::String& source) override; - Rml::TextureHandle GenerateTexture( - Rml::Span source, - Rml::Vector2i dimensions) override; + Rml::TextureHandle GenerateTexture(Rml::Span source, Rml::Vector2i dimensions) override; void ReleaseTexture(Rml::TextureHandle texture) override; @@ -58,7 +52,7 @@ public: bool isScissorEnabled() const { return scissorEnabled_; } Rml::Rectanglei scissorRegion() const { return scissorRect_; } -private: + private: struct CompiledGeom { bgfx::VertexBufferHandle vbh; bgfx::IndexBufferHandle ibh; diff --git a/include/fabric/ui/BgfxSystemInterface.hh b/include/fabric/ui/BgfxSystemInterface.hh index 4ed97ba7..6f90c800 100644 --- a/include/fabric/ui/BgfxSystemInterface.hh +++ b/include/fabric/ui/BgfxSystemInterface.hh @@ -7,13 +7,13 @@ namespace fabric { class BgfxSystemInterface : public Rml::SystemInterface { -public: + public: BgfxSystemInterface(); double GetElapsedTime() override; bool LogMessage(Rml::Log::Type type, const Rml::String& message) override; -private: + private: std::chrono::steady_clock::time_point startTime_; }; diff --git a/include/fabric/ui/WebView.hh b/include/fabric/ui/WebView.hh index fc3666e8..13a06c3c 100644 --- a/include/fabric/ui/WebView.hh +++ b/include/fabric/ui/WebView.hh @@ -3,11 +3,42 @@ // Check if the FABRIC_USE_WEBVIEW macro is defined #if defined(FABRIC_USE_WEBVIEW) #include "webview/webview.h" + +// webview.h transitively includes X11/Xlib.h → X11/X.h on Linux, which +// defines bare-word macros (Always, None, Never, Bool, Status, Success, +// True, False) that collide with identifiers in downstream headers +// (GoogleTest's struct None, Quill's enum members, etc.). +// Undefine them immediately so every TU that includes WebView.hh gets a +// clean namespace. +#ifdef Always +#undef Always +#endif +#ifdef None +#undef None +#endif +#ifdef Never +#undef Never +#endif +#ifdef Bool +#undef Bool +#endif +#ifdef Status +#undef Status +#endif +#ifdef Success +#undef Success #endif +#ifdef True +#undef True +#endif +#ifdef False +#undef False +#endif +#endif // FABRIC_USE_WEBVIEW -#include -#include #include +#include +#include namespace fabric { @@ -18,93 +49,91 @@ namespace fabric { * a clean interface for working with the embedded web browser. */ class WebView { -public: - /** - * @brief Construct a new WebView object - * - * @param title Window title - * @param width Window width - * @param height Window height - * @param debug Enable debug mode (shows developer tools) - * @param createWindow Create actual window (false for testing) - * @param window Parent window handle (nullptr for default) - */ - WebView(const std::string &title = "Fabric", int width = 800, - int height = 600, bool debug = false, bool createWindow = true, - void *window = nullptr); - - /** - * @brief Set the window title - * - * @param title Window title - */ - void setTitle(const std::string &title); - - /** - * @brief Set the window size - * - * @param width Window width - * @param height Window height - * @param hint Size hint (WEBVIEW_HINT_NONE, WEBVIEW_HINT_MIN, - * WEBVIEW_HINT_MAX, WEBVIEW_HINT_FIXED) - */ + public: + /** + * @brief Construct a new WebView object + * + * @param title Window title + * @param width Window width + * @param height Window height + * @param debug Enable debug mode (shows developer tools) + * @param createWindow Create actual window (false for testing) + * @param window Parent window handle (nullptr for default) + */ + WebView(const std::string& title = "Fabric", int width = 800, int height = 600, bool debug = false, + bool createWindow = true, void* window = nullptr); + + /** + * @brief Set the window title + * + * @param title Window title + */ + void setTitle(const std::string& title); + + /** + * @brief Set the window size + * + * @param width Window width + * @param height Window height + * @param hint Size hint (WEBVIEW_HINT_NONE, WEBVIEW_HINT_MIN, + * WEBVIEW_HINT_MAX, WEBVIEW_HINT_FIXED) + */ #if defined(FABRIC_USE_WEBVIEW) - void setSize(int width, int height, webview_hint_t hint = WEBVIEW_HINT_NONE); + void setSize(int width, int height, webview_hint_t hint = WEBVIEW_HINT_NONE); #else - void setSize(int width, int height, int hint = 0); + void setSize(int width, int height, int hint = 0); #endif - /** - * @brief Navigate to a URL - * - * @param url URL to navigate to - */ - void navigate(const std::string &url); - - /** - * @brief Set HTML content directly - * - * @param html HTML content - */ - void setHTML(const std::string &html); - - /** - * @brief Run the main event loop - */ - void run(); - - /** - * @brief Terminate the main event loop - */ - void terminate(); - - /** - * @brief Evaluate JavaScript in the webview - * - * @param js JavaScript code to evaluate - */ - void eval(const std::string &js); - - /** - * @brief Bind a native C++ callback to be callable from JavaScript - * - * @param name Name of the function in JavaScript - * @param fn Callback function - */ - void bind(const std::string &name, - const std::function &fn); - -protected: - // These fields are protected to allow testing - std::string title; - int width; - int height; - bool debug; - std::string html; - -private: + /** + * @brief Navigate to a URL + * + * @param url URL to navigate to + */ + void navigate(const std::string& url); + + /** + * @brief Set HTML content directly + * + * @param html HTML content + */ + void setHTML(const std::string& html); + + /** + * @brief Run the main event loop + */ + void run(); + + /** + * @brief Terminate the main event loop + */ + void terminate(); + + /** + * @brief Evaluate JavaScript in the webview + * + * @param js JavaScript code to evaluate + */ + void eval(const std::string& js); + + /** + * @brief Bind a native C++ callback to be callable from JavaScript + * + * @param name Name of the function in JavaScript + * @param fn Callback function + */ + void bind(const std::string& name, const std::function& fn); + + protected: + // These fields are protected to allow testing + std::string title; + int width; + int height; + bool debug; + std::string html; + + private: #if defined(FABRIC_USE_WEBVIEW) - std::unique_ptr webview_; + std::unique_ptr webview_; #endif }; diff --git a/include/fabric/utils/BVH.hh b/include/fabric/utils/BVH.hh index c86683ff..ee827b3c 100644 --- a/include/fabric/utils/BVH.hh +++ b/include/fabric/utils/BVH.hh @@ -1,16 +1,15 @@ #pragma once #include "fabric/core/Rendering.hh" -#include #include #include #include +#include namespace fabric { -template -class BVH { -public: +template class BVH { + public: void insert(const AABB& bounds, T data) { items_.push_back({bounds, std::move(data)}); dirty_ = true; @@ -84,7 +83,7 @@ public: size_t size() const { return items_.size(); } bool empty() const { return items_.empty(); } -private: + private: struct Item { AABB bounds; T data; @@ -122,18 +121,21 @@ private: float dz = nodeBounds.max.z - nodeBounds.min.z; int axis = 0; - if (dy > dx && dy > dz) axis = 1; - else if (dz > dx && dz > dy) axis = 2; + if (dy > dx && dy > dz) + axis = 1; + else if (dz > dx && dz > dy) + axis = 2; // Sort indices by centroid along chosen axis - std::sort(indices.begin() + start, indices.begin() + end, - [&](int a, int b) { - Vec3f ca = items_[a].bounds.center(); - Vec3f cb = items_[b].bounds.center(); - if (axis == 0) return ca.x < cb.x; - if (axis == 1) return ca.y < cb.y; - return ca.z < cb.z; - }); + std::sort(indices.begin() + start, indices.begin() + end, [&](int a, int b) { + Vec3f ca = items_[a].bounds.center(); + Vec3f cb = items_[b].bounds.center(); + if (axis == 0) + return ca.x < cb.x; + if (axis == 1) + return ca.y < cb.y; + return ca.z < cb.z; + }); int mid = start + (end - start) / 2; @@ -163,8 +165,10 @@ private: return; } - if (node.left >= 0) queryRecursive(node.left, region, results); - if (node.right >= 0) queryRecursive(node.right, region, results); + if (node.left >= 0) + queryRecursive(node.left, region, results); + if (node.right >= 0) + queryRecursive(node.right, region, results); } void queryFrustumRecursive(int nodeIndex, const Frustum& frustum, std::vector& results) const { @@ -180,19 +184,15 @@ private: return; } - if (node.left >= 0) queryFrustumRecursive(node.left, frustum, results); - if (node.right >= 0) queryFrustumRecursive(node.right, frustum, results); + if (node.left >= 0) + queryFrustumRecursive(node.left, frustum, results); + if (node.right >= 0) + queryFrustumRecursive(node.right, frustum, results); } static AABB unionAABB(const AABB& a, const AABB& b) { - return AABB( - Vec3f(std::min(a.min.x, b.min.x), - std::min(a.min.y, b.min.y), - std::min(a.min.z, b.min.z)), - Vec3f(std::max(a.max.x, b.max.x), - std::max(a.max.y, b.max.y), - std::max(a.max.z, b.max.z)) - ); + return AABB(Vec3f(std::min(a.min.x, b.min.x), std::min(a.min.y, b.min.y), std::min(a.min.z, b.min.z)), + Vec3f(std::max(a.max.x, b.max.x), std::max(a.max.y, b.max.y), std::max(a.max.z, b.max.z))); } }; diff --git a/include/fabric/utils/BufferPool.hh b/include/fabric/utils/BufferPool.hh index b8f4e0e9..a1a1ecbf 100644 --- a/include/fabric/utils/BufferPool.hh +++ b/include/fabric/utils/BufferPool.hh @@ -17,126 +17,125 @@ class BufferPool; // Move-only RAII handle for a borrowed buffer slot. // Automatically returns the slot to the pool on destruction. class BufferSlot { -public: - BufferSlot() : pool_(nullptr), data_(nullptr), size_(0) {} - ~BufferSlot(); - - BufferSlot(BufferSlot&& other) noexcept - : pool_(other.pool_), data_(other.data_), size_(other.size_) { - other.pool_ = nullptr; - other.data_ = nullptr; - other.size_ = 0; - } - - BufferSlot& operator=(BufferSlot&& other) noexcept { - if (this != &other) { - release(); - pool_ = other.pool_; - data_ = other.data_; - size_ = other.size_; - other.pool_ = nullptr; - other.data_ = nullptr; - other.size_ = 0; + public: + BufferSlot() : pool_(nullptr), data_(nullptr), size_(0) {} + ~BufferSlot(); + + BufferSlot(BufferSlot&& other) noexcept : pool_(other.pool_), data_(other.data_), size_(other.size_) { + other.pool_ = nullptr; + other.data_ = nullptr; + other.size_ = 0; } - return *this; - } - BufferSlot(const BufferSlot&) = delete; - BufferSlot& operator=(const BufferSlot&) = delete; + BufferSlot& operator=(BufferSlot&& other) noexcept { + if (this != &other) { + release(); + pool_ = other.pool_; + data_ = other.data_; + size_ = other.size_; + other.pool_ = nullptr; + other.data_ = nullptr; + other.size_ = 0; + } + return *this; + } + + BufferSlot(const BufferSlot&) = delete; + BufferSlot& operator=(const BufferSlot&) = delete; - uint8_t* data() { return data_; } - const uint8_t* data() const { return data_; } - size_t size() const { return size_; } - std::span span() { return {data_, size_}; } - std::span span() const { return {data_, size_}; } + uint8_t* data() { return data_; } + const uint8_t* data() const { return data_; } + size_t size() const { return size_; } + std::span span() { return {data_, size_}; } + std::span span() const { return {data_, size_}; } - explicit operator bool() const { return data_ != nullptr; } + explicit operator bool() const { return data_ != nullptr; } -private: - friend class BufferPool; - BufferSlot(BufferPool* pool, uint8_t* data, size_t size) - : pool_(pool), data_(data), size_(size) {} + private: + friend class BufferPool; + BufferSlot(BufferPool* pool, uint8_t* data, size_t size) : pool_(pool), data_(data), size_(size) {} - void release(); + void release(); - BufferPool* pool_; - uint8_t* data_; - size_t size_; + BufferPool* pool_; + uint8_t* data_; + size_t size_; }; // Fixed-size slab allocator with borrow/return semantics. // All memory is pre-allocated in a single contiguous block. // Thread-safe: blocking borrow() and non-blocking tryBorrow(). class BufferPool { -public: - BufferPool(size_t slotSize, size_t slotCount) - : slotSize_(slotSize), slotCount_(slotCount) { - storage_.resize(slotSize * slotCount); - freeSlots_.reserve(slotCount); - for (size_t i = slotCount; i > 0; --i) { - freeSlots_.push_back(storage_.data() + (i - 1) * slotSize); + public: + BufferPool(size_t slotSize, size_t slotCount) : slotSize_(slotSize), slotCount_(slotCount) { + storage_.resize(slotSize * slotCount); + freeSlots_.reserve(slotCount); + for (size_t i = slotCount; i > 0; --i) { + freeSlots_.push_back(storage_.data() + (i - 1) * slotSize); + } + } + + // Blocking borrow: waits until a slot is available. + BufferSlot borrow() { + std::unique_lock lock(mutex_); + cv_.wait(lock, [this] { return !freeSlots_.empty(); }); + return popSlot(); } - } - - // Blocking borrow: waits until a slot is available. - BufferSlot borrow() { - std::unique_lock lock(mutex_); - cv_.wait(lock, [this] { return !freeSlots_.empty(); }); - return popSlot(); - } - - // Non-blocking borrow: returns nullopt if pool is exhausted. - std::optional tryBorrow() { - std::lock_guard lock(mutex_); - if (freeSlots_.empty()) { - return std::nullopt; + + // Non-blocking borrow: returns nullopt if pool is exhausted. + std::optional tryBorrow() { + std::lock_guard lock(mutex_); + if (freeSlots_.empty()) { + return std::nullopt; + } + return popSlot(); } - return popSlot(); - } - - size_t available() const { - std::lock_guard lock(mutex_); - return freeSlots_.size(); - } - - size_t capacity() const { return slotCount_; } - size_t slotSize() const { return slotSize_; } - -private: - friend class BufferSlot; - - BufferSlot popSlot() { - uint8_t* ptr = freeSlots_.back(); - freeSlots_.pop_back(); - return BufferSlot(this, ptr, slotSize_); - } - - void returnSlot(uint8_t* ptr) { - { - std::lock_guard lock(mutex_); - freeSlots_.push_back(ptr); + + size_t available() const { + std::lock_guard lock(mutex_); + return freeSlots_.size(); } - cv_.notify_one(); - } - - size_t slotSize_; - size_t slotCount_; - std::vector storage_; - std::vector freeSlots_; - mutable std::mutex mutex_; - std::condition_variable cv_; + + size_t capacity() const { return slotCount_; } + size_t slotSize() const { return slotSize_; } + + private: + friend class BufferSlot; + + BufferSlot popSlot() { + uint8_t* ptr = freeSlots_.back(); + freeSlots_.pop_back(); + return BufferSlot(this, ptr, slotSize_); + } + + void returnSlot(uint8_t* ptr) { + { + std::lock_guard lock(mutex_); + freeSlots_.push_back(ptr); + } + cv_.notify_one(); + } + + size_t slotSize_; + size_t slotCount_; + std::vector storage_; + std::vector freeSlots_; + mutable std::mutex mutex_; + std::condition_variable cv_; }; // Inline definitions for BufferSlot that depend on BufferPool -inline BufferSlot::~BufferSlot() { release(); } +inline BufferSlot::~BufferSlot() { + release(); +} inline void BufferSlot::release() { - if (pool_ && data_) { - pool_->returnSlot(data_); - pool_ = nullptr; - data_ = nullptr; - size_ = 0; - } + if (pool_ && data_) { + pool_->returnSlot(data_); + pool_ = nullptr; + data_ = nullptr; + size_ = 0; + } } } // namespace fabric diff --git a/include/fabric/utils/CoordinatedGraph.hh b/include/fabric/utils/CoordinatedGraph.hh index ada146b9..5c571063 100644 --- a/include/fabric/utils/CoordinatedGraph.hh +++ b/include/fabric/utils/CoordinatedGraph.hh @@ -1,23 +1,24 @@ #pragma once -#include -#include -#include -#include -#include -#include -#include +#include #include -#include -#include -#include -#include -#include #include -#include #include +#include +#include +#include #include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include +#include namespace fabric { @@ -31,51 +32,47 @@ namespace fabric { * @brief Exception thrown when a cycle is detected in the graph */ class CycleDetectedException : public std::runtime_error { -public: - explicit CycleDetectedException(const std::string& message) - : std::runtime_error(message) {} + public: + explicit CycleDetectedException(const std::string& message) : std::runtime_error(message) {} }; /** * @brief Exception thrown when a lock cannot be acquired */ class LockAcquisitionException : public std::runtime_error { -public: - explicit LockAcquisitionException(const std::string& message) - : std::runtime_error(message) {} + public: + explicit LockAcquisitionException(const std::string& message) : std::runtime_error(message) {} }; /** * @brief Exception thrown when a deadlock is detected in the lock graph */ class DeadlockDetectedException : public std::runtime_error { -public: - explicit DeadlockDetectedException(const std::string& message) - : std::runtime_error(message) {} + public: + explicit DeadlockDetectedException(const std::string& message) : std::runtime_error(message) {} }; /** * @brief Exception thrown when a lock acquisition times out */ class LockTimeoutException : public std::runtime_error { -public: - explicit LockTimeoutException(const std::string& message) - : std::runtime_error(message) {} + public: + explicit LockTimeoutException(const std::string& message) : std::runtime_error(message) {} }; /** * @brief A thread-safe directed acyclic graph implementation with intentional locking - * + * * This graph implementation is designed for concurrent access with node-level * locking for maximum parallelism, while providing awareness of different * lock types and their intentions. It enables high-priority graph operations * to coordinate with lower-priority node operations. - * + * * The graph enforces acyclicity to serve as a proper DAG (Directed Acyclic Graph), * which is essential for preventing deadlocks in dependency management. * It implements resource lock management with deadlock prevention through lock * ordering based on the graph structure. - * + * * Key Features: * - Enforced acyclicity with cycle detection on all edge additions * - Intent-based locking (read, write, structure modification) @@ -85,57 +82,56 @@ public: * - Thread-safe node access through explicit locking mechanisms * - Awareness propagation between locks of different levels * - Lock resource management with deadlock prevention - * + * * @tparam T Type of data stored in graph nodes * @tparam KeyType Type used as node identifier (default: std::string) */ -template -class CoordinatedGraph { -public: +template class CoordinatedGraph { + public: /** * @brief Lock intent type to specify the purpose of a lock */ - enum class LockIntent { - Read, // Intent to read without modification - NodeModify, // Intent to modify node data only - GraphStructure, // Intent to modify graph structure (highest priority) + enum class LockIntent : std::uint8_t { + Read, // Intent to read without modification + NodeModify, // Intent to modify node data only + GraphStructure, // Intent to modify graph structure (highest priority) }; /** * @brief Status of a lock for notification callbacks */ - enum class LockStatus { - Acquired, // Lock has been acquired - Released, // Lock has been released - Preempted, // Lock has been preempted by higher priority - BackgroundWait, // Lock is temporarily waiting for structural changes - Failed // Lock acquisition failed + enum class LockStatus : std::uint8_t { + Acquired, // Lock has been acquired + Released, // Lock has been released + Preempted, // Lock has been preempted by higher priority + BackgroundWait, // Lock is temporarily waiting for structural changes + Failed // Lock acquisition failed }; - + /** * @brief Lock acquisition mode for resource locks */ - enum class LockMode { - Shared, // Multiple readers allowed - Exclusive, // Single writer, no readers - Upgrade, // Initially shared, can be upgraded to exclusive + enum class LockMode : std::uint8_t { + Shared, // Multiple readers allowed + Exclusive, // Single writer, no readers + Upgrade, // Initially shared, can be upgraded to exclusive }; /** * @brief Status of a resource lock */ - enum class ResourceLockStatus { - Unlocked, // Not locked - Shared, // Held in shared mode - Exclusive, // Held in exclusive mode - Intention, // Held in intention mode - Pending // Lock acquisition in progress + enum class ResourceLockStatus : std::uint8_t { + Unlocked, // Not locked + Shared, // Held in shared mode + Exclusive, // Held in exclusive mode + Intention, // Held in intention mode + Pending // Lock acquisition in progress }; /** * @brief Node states used for traversal algorithms */ - enum class NodeState { + enum class NodeState : std::uint8_t { Unvisited, Visiting, Visited @@ -148,16 +144,16 @@ public: /** * @brief A node in the graph - * + * * Each node has its own lock for fine-grained concurrency control. */ class Node { - public: + public: using LockCallback = std::function; /** * @brief Construct a node with the given data - * + * * @param key Node identifier * @param data Node data */ @@ -166,7 +162,7 @@ public: /** * @brief Get the node's key - * + * * @return Node key */ const KeyType& getKey() const { return key_; } @@ -201,23 +197,21 @@ public: lastAccessTime_ = std::chrono::steady_clock::now(); return data_; } - + /** * @brief Get the node's data without acquiring a lock (const version) - * + * * This method should only be used when the caller already holds a lock on the node. - * + * * @return Reference to the data */ - const T& getDataNoLock() const { - return data_; - } + const T& getDataNoLock() const { return data_; } /** * @brief Get the node's data without acquiring a lock (mutable version) - * + * * This method should only be used when the caller already holds a lock on the node. - * + * * @return Reference to the data */ T& getDataNoLock() { @@ -227,7 +221,7 @@ public: /** * @brief Set the node's data - * + * * @param data New data */ void setData(T data) { @@ -238,7 +232,7 @@ public: /** * @brief Get the node's last access time - * + * * @return Last access time */ std::chrono::steady_clock::time_point getLastAccessTime() const { @@ -256,87 +250,64 @@ public: /** * @brief Try to acquire a lock with specified intent and timeout - * + * * @param intent Purpose of the lock * @param timeoutMs Timeout in milliseconds (default: 100ms) * @param callback Function to call when lock status changes * @return A lock handle or nullptr if acquisition failed */ - std::unique_ptr tryLock( - LockIntent intent, - size_t timeoutMs = 100, - LockCallback callback = nullptr - ) { + std::unique_ptr tryLock(LockIntent intent, size_t timeoutMs = 100, + LockCallback callback = nullptr) { // This implementation is inlined to avoid the helper function issues using namespace std::chrono; - + // For read locks if (intent == LockIntent::Read) { std::shared_lock lock(mutex_, std::try_to_lock); if (lock.owns_lock()) { - return std::make_unique( - this, - std::move(lock), - intent, - callback - ); + return std::make_unique(this, std::move(lock), intent, callback); } - + // If immediate acquisition failed, try with timeout auto start = steady_clock::now(); while (true) { lock = std::shared_lock(mutex_, std::try_to_lock); if (lock.owns_lock()) { - return std::make_unique( - this, - std::move(lock), - intent, - callback - ); + return std::make_unique(this, std::move(lock), intent, callback); } - + if (duration_cast(steady_clock::now() - start).count() >= timeoutMs) { return nullptr; } - + std::this_thread::sleep_for(milliseconds(1)); } - } + } // For write locks else { std::unique_lock lock(mutex_, std::try_to_lock); if (lock.owns_lock()) { - return std::make_unique( - this, - std::move(lock), - intent, - callback - ); + return std::make_unique(this, std::move(lock), intent, callback); } - + // If immediate acquisition failed, try with timeout auto start = steady_clock::now(); while (true) { lock = std::unique_lock(mutex_, std::try_to_lock); if (lock.owns_lock()) { - return std::make_unique( - this, - std::move(lock), - intent, - callback - ); + return std::make_unique(this, std::move(lock), intent, callback); } - + if (duration_cast(steady_clock::now() - start).count() >= timeoutMs) { return nullptr; } - + std::this_thread::sleep_for(milliseconds(1)); } } } - private: + private: friend class CoordinatedGraph; friend class NodeLockHandle; @@ -346,7 +317,7 @@ public: mutable std::shared_mutex mutex_; std::vector> activeCallbacks_; std::mutex callbackMutex_; - + // Method to notify all lock holders with callbacks void notifyLockHolders(LockStatus status) { std::lock_guard lock(callbackMutex_); @@ -356,17 +327,19 @@ public: } } } - + // Register a callback for this lock void registerCallback(LockIntent intent, LockCallback callback) { - if (!callback) return; + if (!callback) + return; std::lock_guard lock(callbackMutex_); activeCallbacks_.push_back({intent, callback}); } - + // Remove a callback void removeCallback(LockIntent intent, LockCallback callback) { - if (!callback) return; + if (!callback) + return; std::lock_guard lock(callbackMutex_); // Since we can't directly compare std::function objects, use a simpler approach // Just remove all callbacks with the same intent (this works because we register/remove @@ -386,45 +359,39 @@ public: * @brief A handle for a node lock that automatically releases on destruction */ class NodeLockHandle { - public: + public: /** * @brief Constructor for a read lock */ - NodeLockHandle( - Node* node, - std::shared_lock lock, - LockIntent intent, - typename Node::LockCallback callback - ) : node_(node), - readLock_(std::move(lock)), - writeLock_(), - isReadLock_(true), - intent_(intent), - callback_(callback) { + NodeLockHandle(Node* node, std::shared_lock lock, LockIntent intent, + typename Node::LockCallback callback) + : node_(node), + readLock_(std::move(lock)), + writeLock_(), + isReadLock_(true), + intent_(intent), + callback_(callback) { if (node_ && callback_) { node_->registerCallback(intent_, callback_); } } - + /** * @brief Constructor for a write lock */ - NodeLockHandle( - Node* node, - std::unique_lock lock, - LockIntent intent, - typename Node::LockCallback callback - ) : node_(node), - readLock_(), - writeLock_(std::move(lock)), - isReadLock_(false), - intent_(intent), - callback_(callback) { + NodeLockHandle(Node* node, std::unique_lock lock, LockIntent intent, + typename Node::LockCallback callback) + : node_(node), + readLock_(), + writeLock_(std::move(lock)), + isReadLock_(false), + intent_(intent), + callback_(callback) { if (node_ && callback_) { node_->registerCallback(intent_, callback_); } } - + /** * @brief Destructor that releases the lock */ @@ -433,16 +400,14 @@ public: node_->removeCallback(intent_, callback_); } } - + /** * @brief Check if the lock is currently held - * + * * @return true if the lock is held, false otherwise */ - bool isLocked() const { - return isReadLock_ ? readLock_.owns_lock() : writeLock_.owns_lock(); - } - + bool isLocked() const { return isReadLock_ ? readLock_.owns_lock() : writeLock_.owns_lock(); } + /** * @brief Release the lock early (before destruction) */ @@ -452,32 +417,28 @@ public: } else { writeLock_.unlock(); } - + if (node_ && callback_) { node_->removeCallback(intent_, callback_); callback_ = nullptr; } } - + /** * @brief Get the node this lock is for - * + * * @return Pointer to the node */ - Node* getNode() const { - return node_; - } - + Node* getNode() const { return node_; } + /** * @brief Get the intent of this lock - * + * * @return Intent of the lock */ - LockIntent getIntent() const { - return intent_; - } - - private: + LockIntent getIntent() const { return intent_; } + + private: Node* node_; std::shared_lock readLock_; std::unique_lock writeLock_; @@ -490,49 +451,31 @@ public: * @brief A handle for a graph lock that automatically releases on destruction */ class GraphLockHandle { - public: + public: /** * @brief Constructor for a read lock */ - GraphLockHandle( - CoordinatedGraph* graph, - std::shared_lock lock, - LockIntent intent - ) : graph_(graph), - readLock_(std::move(lock)), - writeLock_(), - isReadLock_(true), - intent_(intent) {} - + GraphLockHandle(CoordinatedGraph* graph, std::shared_lock lock, LockIntent intent) + : graph_(graph), readLock_(std::move(lock)), writeLock_(), isReadLock_(true), intent_(intent) {} + /** * @brief Constructor for a write lock */ - GraphLockHandle( - CoordinatedGraph* graph, - std::unique_lock lock, - LockIntent intent - ) : graph_(graph), - readLock_(), - writeLock_(std::move(lock)), - isReadLock_(false), - intent_(intent) {} - + GraphLockHandle(CoordinatedGraph* graph, std::unique_lock lock, LockIntent intent) + : graph_(graph), readLock_(), writeLock_(std::move(lock)), isReadLock_(false), intent_(intent) {} + /** * @brief Destructor that releases the lock */ - ~GraphLockHandle() { - release(); - } - + ~GraphLockHandle() { release(); } + /** * @brief Check if the lock is currently held - * + * * @return true if the lock is held, false otherwise */ - bool isLocked() const { - return isReadLock_ ? readLock_.owns_lock() : writeLock_.owns_lock(); - } - + bool isLocked() const { return isReadLock_ ? readLock_.owns_lock() : writeLock_.owns_lock(); } + /** * @brief Release the lock early (before destruction) */ @@ -553,24 +496,22 @@ public: } } } - + /** * @brief Get the intent of this lock - * + * * @return Intent of the lock */ - LockIntent getIntent() const { - return intent_; - } - - private: + LockIntent getIntent() const { return intent_; } + + private: CoordinatedGraph* graph_; std::shared_lock readLock_; std::unique_lock writeLock_; bool isReadLock_; LockIntent intent_; }; - + /** * @brief A handle for a resource lock that automatically releases on destruction * @@ -579,29 +520,23 @@ public: * through the DAG ordering. */ class ResourceLockHandle { - public: + public: /** * @brief Constructor for a resource lock handle */ - ResourceLockHandle( - CoordinatedGraph* graph, - KeyType resourceKey, - LockMode mode, - ResourceLockStatus status, - std::thread::id ownerId - ) : graph_(graph), - resourceKey_(std::move(resourceKey)), - mode_(mode), - status_(status), - ownerId_(ownerId), - isValid_(true) {} + ResourceLockHandle(CoordinatedGraph* graph, KeyType resourceKey, LockMode mode, ResourceLockStatus status, + std::thread::id ownerId) + : graph_(graph), + resourceKey_(std::move(resourceKey)), + mode_(mode), + status_(status), + ownerId_(ownerId), + isValid_(true) {} /** * @brief Destructor releases the lock if still held */ - ~ResourceLockHandle() { - release(); - } + ~ResourceLockHandle() { release(); } /** * @brief Release the lock early (before destruction) @@ -618,13 +553,12 @@ public: /** * @brief Upgrade the lock from shared to exclusive if possible - * + * * @param timeoutMs Timeout in milliseconds * @return true if upgrade succeeded, false otherwise */ bool upgrade(size_t timeoutMs = 100) { - if (!isValid_ || mode_ != LockMode::Upgrade || - status_ != ResourceLockStatus::Shared) { + if (!isValid_ || mode_ != LockMode::Upgrade || status_ != ResourceLockStatus::Shared) { return false; } @@ -641,32 +575,24 @@ public: /** * @brief Check if the lock is currently held */ - bool isLocked() const { - return isValid_ && status_ != ResourceLockStatus::Unlocked; - } + bool isLocked() const { return isValid_ && status_ != ResourceLockStatus::Unlocked; } /** * @brief Get the current lock status */ - ResourceLockStatus getStatus() const { - return status_; - } + ResourceLockStatus getStatus() const { return status_; } /** * @brief Get the lock mode */ - LockMode getMode() const { - return mode_; - } + LockMode getMode() const { return mode_; } /** * @brief Get the resource key this lock is for */ - const KeyType& getResourceKey() const { - return resourceKey_; - } + const KeyType& getResourceKey() const { return resourceKey_; } - private: + private: CoordinatedGraph* graph_; KeyType resourceKey_; LockMode mode_; @@ -680,7 +606,7 @@ public: /** * @brief Add a node to the graph - * + * * @param key Node identifier * @param data Node data * @return true if node was added, false if a node with this key already exists @@ -690,22 +616,22 @@ public: if (!lock || !lock->isLocked()) { throw LockAcquisitionException("Failed to acquire graph lock for node addition"); } - + if (nodes_.find(key) != nodes_.end()) { return false; } - + auto node = std::make_shared(key, std::move(data)); nodes_[key] = node; outEdges_[key] = std::unordered_set(); inEdges_[key] = std::unordered_set(); - + return true; } /** * @brief Remove a node from the graph - * + * * @param key Node identifier * @return true if node was removed, false if it didn't exist */ @@ -714,40 +640,40 @@ public: if (!lock || !lock->isLocked()) { throw LockAcquisitionException("Failed to acquire graph lock for node removal"); } - + if (nodes_.find(key) == nodes_.end()) { return false; } - + // Notify node lock holders about the impending removal auto nodePtr = nodes_[key]; if (nodePtr) { nodePtr->notifyLockHolders(LockStatus::Preempted); } - + // Remove all edges connected to this node for (const auto& target : outEdges_[key]) { inEdges_[target].erase(key); } - + for (const auto& source : inEdges_[key]) { outEdges_[source].erase(key); } - + // Remove the node nodes_.erase(key); outEdges_.erase(key); inEdges_.erase(key); - + // Signal that a node was removed (for anyone who needs to know) onNodeRemoved(key); - + return true; } /** * @brief Check if a node exists - * + * * @param key Node identifier * @return true if the node exists, false otherwise * @throws LockAcquisitionException If the graph lock cannot be acquired @@ -757,13 +683,13 @@ public: if (!lock || !lock->isLocked()) { throw LockAcquisitionException("Failed to acquire graph lock for node check"); } - + return nodes_.find(key) != nodes_.end(); } /** * @brief Get a node by key with timeout protection - * + * * @param key Node identifier * @param timeoutMs Timeout in milliseconds (default: 100ms) * @return Shared pointer to the node or nullptr if not found or timed out @@ -774,14 +700,14 @@ public: if (!lock || !lock->isLocked()) { throw LockAcquisitionException("Failed to acquire graph lock for getting node"); } - + auto it = nodes_.find(key); return (it != nodes_.end()) ? it->second : nullptr; } /** * @brief Try to lock a specific node with an explicit intent - * + * * @param key Node identifier * @param intent Purpose of the lock * @param forWrite Whether to acquire a write lock @@ -789,103 +715,86 @@ public: * @param callback Function to call when lock status changes * @return A lock handle or nullptr if acquisition failed */ - std::unique_ptr tryLockNode( - const KeyType& key, - LockIntent intent, - bool forWrite = false, - size_t timeoutMs = 100, - typename Node::LockCallback callback = nullptr - ) const { + std::unique_ptr tryLockNode(const KeyType& key, LockIntent intent, bool forWrite = false, + size_t timeoutMs = 100, + typename Node::LockCallback callback = nullptr) const { // Check if we can proceed with this lock intent if (!canProceedWithIntent(intent)) { return nullptr; } - + // Acquire a graph-level read lock to check if the node exists auto graphLock = lockGraph(LockIntent::Read, timeoutMs); if (!graphLock || !graphLock->isLocked()) { return nullptr; } - + auto it = nodes_.find(key); if (it == nodes_.end()) { return nullptr; } - + auto node = it->second; - + // Release graph lock before attempting node lock to prevent deadlocks graphLock->release(); - + // Now try to lock the node return node->tryLock(intent, timeoutMs, callback); } /** * @brief Try to acquire an exclusive lock on the graph for structural changes - * + * * @param intent Purpose of the lock * @param timeoutMs Timeout in milliseconds (default: 100ms) * @return A graph lock handle or nullptr if acquisition failed */ - std::unique_ptr lockGraph( - LockIntent intent, - size_t timeoutMs = 100 - ) const { + std::unique_ptr lockGraph(LockIntent intent, size_t timeoutMs = 100) const { using namespace std::chrono; - + // For read locks, try to acquire immediately if (intent == LockIntent::Read) { std::shared_lock lock(graphMutex_, std::try_to_lock); if (lock.owns_lock()) { - return std::make_unique( - const_cast(this), - std::move(lock), - intent - ); + return std::make_unique(const_cast(this), std::move(lock), intent); } - + // If immediate acquisition failed, try with timeout auto start = steady_clock::now(); while (true) { lock = std::shared_lock(graphMutex_, std::try_to_lock); if (lock.owns_lock()) { - return std::make_unique( - const_cast(this), - std::move(lock), - intent - ); + return std::make_unique(const_cast(this), std::move(lock), + intent); } - + if (duration_cast(steady_clock::now() - start).count() >= timeoutMs) { return nullptr; } - + std::this_thread::sleep_for(milliseconds(1)); } - } + } // For write/structure locks, need to notify existing holders else { // If this is a structure lock, notify all node lock holders if (intent == LockIntent::GraphStructure) { const_cast(this)->notifyAllNodeLockHolders(LockStatus::BackgroundWait); } - + // Try to acquire the write lock std::unique_lock lock(graphMutex_, std::try_to_lock); if (lock.owns_lock()) { // Record the current structural operation intent if (intent == LockIntent::GraphStructure) { - const_cast(this)->currentStructuralIntent_.store(static_cast(intent), std::memory_order_release); + const_cast(this)->currentStructuralIntent_.store(static_cast(intent), + std::memory_order_release); } - - return std::make_unique( - const_cast(this), - std::move(lock), - intent - ); - } - + + return std::make_unique(const_cast(this), std::move(lock), intent); + } + // If immediate acquisition failed, try with timeout auto start = steady_clock::now(); while (true) { @@ -893,25 +802,23 @@ public: if (lock.owns_lock()) { // Record the current structural operation intent if (intent == LockIntent::GraphStructure) { - const_cast(this)->currentStructuralIntent_.store(static_cast(intent), std::memory_order_release); + const_cast(this)->currentStructuralIntent_.store(static_cast(intent), + std::memory_order_release); } - - return std::make_unique( - const_cast(this), - std::move(lock), - intent - ); + + return std::make_unique(const_cast(this), std::move(lock), + intent); } - + if (duration_cast(steady_clock::now() - start).count() >= timeoutMs) { // Reset any notifications we sent if (intent == LockIntent::GraphStructure) { const_cast(this)->notifyAllNodeLockHolders(LockStatus::Acquired); } - + return nullptr; } - + std::this_thread::sleep_for(milliseconds(1)); } } @@ -919,10 +826,10 @@ public: /** * @brief Add a directed edge between two nodes - * + * * This method always checks for cycles after adding an edge, regardless of * the detectCycles parameter value. This ensures the graph remains acyclic. - * + * * @param fromKey Source node key * @param toKey Target node key * @param detectCycles Whether to detect cycles (always true for DAG integrity) @@ -935,63 +842,62 @@ public: if (!lock || !lock->isLocked()) { throw LockAcquisitionException("Failed to acquire graph lock for edge addition"); } - + if (nodes_.find(fromKey) == nodes_.end() || nodes_.find(toKey) == nodes_.end()) { return false; } - + if (outEdges_[fromKey].find(toKey) != outEdges_[fromKey].end()) { - return false; // Edge already exists + return false; // Edge already exists } - + // Add the edge to test if it creates a cycle outEdges_[fromKey].insert(toKey); inEdges_[toKey].insert(fromKey); - + // Always check for cycles regardless of the detectCycles parameter value // This ensures the graph maintains its DAG properties bool hasCycleResult = false; - + // Direct cycle detection - more efficient than full graph traversal // Check if this edge creates a path from toKey back to fromKey std::unordered_set visited; std::queue queue; - + queue.push(toKey); visited.insert(toKey); - + while (!queue.empty() && !hasCycleResult) { KeyType current = queue.front(); queue.pop(); - + // If we've reached fromKey, we have a cycle if (current == fromKey) { hasCycleResult = true; break; } - + // Check all outgoing edges from the current node for (const auto& nextNode : outEdges_[current]) { - if (visited.find(nextNode) == visited.end()) { - visited.insert(nextNode); + if (visited.insert(nextNode).second) { queue.push(nextNode); } } } - + if (hasCycleResult) { // Rollback the edge addition outEdges_[fromKey].erase(toKey); inEdges_[toKey].erase(fromKey); throw CycleDetectedException("Adding this edge would create a cycle in the graph"); } - + return true; } /** * @brief Remove a directed edge between two nodes - * + * * @param fromKey Source node key * @param toKey Target node key * @return true if the edge was removed, false if it didn't exist or nodes don't exist @@ -1001,24 +907,24 @@ public: if (!lock || !lock->isLocked()) { throw LockAcquisitionException("Failed to acquire graph lock for edge removal"); } - + if (nodes_.find(fromKey) == nodes_.end() || nodes_.find(toKey) == nodes_.end()) { return false; } - + if (outEdges_[fromKey].find(toKey) == outEdges_[fromKey].end()) { - return false; // Edge doesn't exist + return false; // Edge doesn't exist } - + outEdges_[fromKey].erase(toKey); inEdges_[toKey].erase(fromKey); - + return true; } /** * @brief Check if an edge exists - * + * * @param fromKey Source node key * @param toKey Target node key * @return true if the edge exists, false otherwise @@ -1029,20 +935,20 @@ public: if (!lock || !lock->isLocked()) { throw LockAcquisitionException("Failed to acquire graph lock for edge check"); } - + if (nodes_.find(fromKey) == nodes_.end() || nodes_.find(toKey) == nodes_.end()) { return false; } - + return outEdges_.at(fromKey).find(toKey) != outEdges_.at(fromKey).end(); } /** * @brief Get all outgoing edges from a node - * + * * This method retrieves all outgoing edges from the specified node. * If the node doesn't exist, it returns an empty set. - * + * * @param key Node identifier * @return Set of target node keys * @throws LockAcquisitionException If the graph lock cannot be acquired @@ -1052,20 +958,20 @@ public: if (!lock || !lock->isLocked()) { throw LockAcquisitionException("Failed to acquire graph lock for retrieving outgoing edges"); } - + if (outEdges_.find(key) == outEdges_.end()) { return {}; } - + return outEdges_.at(key); } /** * @brief Get all incoming edges to a node - * + * * This method retrieves all incoming edges to the specified node. * If the node doesn't exist, it returns an empty set. - * + * * @param key Node identifier * @return Set of source node keys * @throws LockAcquisitionException If the graph lock cannot be acquired @@ -1075,17 +981,17 @@ public: if (!lock || !lock->isLocked()) { throw LockAcquisitionException("Failed to acquire graph lock for retrieving incoming edges"); } - + if (inEdges_.find(key) == inEdges_.end()) { return {}; } - + return inEdges_.at(key); } /** * @brief Check if the graph has any cycles - * + * * @return true if the graph has cycles, false otherwise * @throws LockAcquisitionException If the graph lock cannot be acquired */ @@ -1094,14 +1000,14 @@ public: if (!lock || !lock->isLocked()) { throw LockAcquisitionException("Failed to acquire graph lock for cycle detection"); } - + // If the graph is empty or has only one node, it can't have cycles if (nodes_.size() <= 1) { return false; } - + std::unordered_map visited; - + for (const auto& node : nodes_) { if (visited.find(node.first) == visited.end()) { if (hasCycleInternal(node.first, visited)) { @@ -1109,13 +1015,13 @@ public: } } } - + return false; } /** * @brief Perform a topological sort of the graph - * + * * @return Vector of node keys in topological order or empty vector if the graph has cycles */ std::vector topologicalSort() const { @@ -1123,21 +1029,21 @@ public: // long-term lock holding during graph traversal std::unordered_map> localOutEdges; std::unordered_set localNodes; - + auto lock = lockGraph(LockIntent::Read); if (!lock || !lock->isLocked()) { throw LockAcquisitionException("Failed to acquire graph lock for topological sort"); } - + // If the graph is empty, return an empty result if (nodes_.empty()) { return {}; } - + // Create local copies of the graph structure for (const auto& [key, _] : nodes_) { localNodes.insert(key); - + auto edgeIt = outEdges_.find(key); if (edgeIt != outEdges_.end()) { localOutEdges[key] = edgeIt->second; @@ -1145,63 +1051,63 @@ public: localOutEdges[key] = {}; } } - + // Release the lock before performing the sort lock->release(); - + // Now perform the topological sort using our local copies std::vector result; std::unordered_map visited; std::unordered_map inProcess; - + std::function visit = [&](const KeyType& key) { if (inProcess[key]) { - return false; // Cycle detected + return false; // Cycle detected } - + if (visited[key]) { return true; } - + inProcess[key] = true; - + // Use local graph structure auto edgeIt = localOutEdges.find(key); if (edgeIt != localOutEdges.end()) { for (const auto& neighbor : edgeIt->second) { // Check if neighbor exists in local nodes if (localNodes.find(neighbor) == localNodes.end()) { - continue; // Skip non-existent nodes + continue; // Skip non-existent nodes } - + if (!visit(neighbor)) { return false; } } } - + inProcess[key] = false; visited[key] = true; result.push_back(key); - + return true; }; - + for (const auto& node : localNodes) { if (!visited[node]) { if (!visit(node)) { - return {}; // Cycle detected + return {}; // Cycle detected } } } - + std::reverse(result.begin(), result.end()); return result; } /** * @brief Execute a function with automatic node locking - * + * * @param key Node identifier * @param func Function to execute with the node data * @param forWrite Whether to acquire a write lock @@ -1212,16 +1118,16 @@ public: bool withNode(const KeyType& key, Func&& func, bool forWrite = false, size_t timeoutMs = 100) { auto intent = forWrite ? LockIntent::NodeModify : LockIntent::Read; auto nodeLock = tryLockNode(key, intent, forWrite, timeoutMs); - + if (!nodeLock || !nodeLock->isLocked()) { return false; } - + auto node = nodeLock->getNode(); if (!node) { return false; } - + if constexpr (std::is_invocable_v) { if (forWrite) { func(node->getDataNoLock()); @@ -1236,7 +1142,7 @@ public: static_assert(std::is_invocable_v || std::is_invocable_v, "Function must accept either T& or const T&"); } - + return true; } @@ -1255,9 +1161,9 @@ public: if (sortedNodes.empty()) { auto lock = lockGraph(LockIntent::Read); if (lock && lock->isLocked() && !nodes_.empty()) { - return false; // Cycle detected + return false; // Cycle detected } - return true; // Empty graph + return true; // Empty graph } // Process nodes in topological order @@ -1280,7 +1186,7 @@ public: /** * @brief Traverse the graph in breadth-first order starting from a node - * + * * @param startKey Key of the starting node * @param visitFunc Function to call for each visited node */ @@ -1288,36 +1194,36 @@ public: // Make local copies of the graph structure to minimize lock duration std::unordered_map> localOutEdges; std::unordered_map> localNodes; - + // Get the starting node and its edges { auto lock = lockGraph(LockIntent::Read); if (!lock || !lock->isLocked()) { throw LockAcquisitionException("Failed to acquire graph lock for BFS"); } - + auto nodeIt = nodes_.find(startKey); if (nodeIt == nodes_.end()) { - return; // Start node doesn't exist + return; // Start node doesn't exist } - + // Copy the nodes and edges we need localNodes[startKey] = nodeIt->second; - + auto edgeIt = outEdges_.find(startKey); if (edgeIt != outEdges_.end()) { localOutEdges[startKey] = edgeIt->second; } } - + // Set up BFS std::queue queue; std::unordered_set visited; - + // Start with the initial node queue.push(startKey); visited.insert(startKey); - + // Process the first node { auto nodeLock = tryLockNode(startKey, LockIntent::Read, false, 50); @@ -1328,19 +1234,19 @@ public: } } } - + // BFS main loop while (!queue.empty()) { KeyType current = queue.front(); queue.pop(); - + // Get the neighbors if we don't already have them locally if (localOutEdges.find(current) == localOutEdges.end()) { auto lock = lockGraph(LockIntent::Read); if (!lock || !lock->isLocked()) { - continue; // Skip if we can't get a lock + continue; // Skip if we can't get a lock } - + auto edgeIt = outEdges_.find(current); if (edgeIt != outEdges_.end()) { localOutEdges[current] = edgeIt->second; @@ -1348,26 +1254,25 @@ public: localOutEdges[current] = {}; } } - + // Process neighbors for (const auto& neighbor : localOutEdges[current]) { - if (visited.find(neighbor) == visited.end()) { - visited.insert(neighbor); + if (visited.insert(neighbor).second) { queue.push(neighbor); - + // Get the node if we don't already have it locally if (localNodes.find(neighbor) == localNodes.end()) { auto lock = lockGraph(LockIntent::Read); if (!lock || !lock->isLocked()) { - continue; // Skip if we can't get a lock + continue; // Skip if we can't get a lock } - + auto nodeIt = nodes_.find(neighbor); if (nodeIt != nodes_.end()) { localNodes[neighbor] = nodeIt->second; } } - + // Visit the neighbor auto nodeLock = tryLockNode(neighbor, LockIntent::Read, false, 50); if (nodeLock && nodeLock->isLocked()) { @@ -1383,50 +1288,50 @@ public: /** * @brief Traverse the graph in depth-first order starting from a node - * + * * @param startKey Key of the starting node * @param visitFunc Function to call for each visited node */ void dfs(const KeyType& startKey, std::function visitFunc) const { // Make local copies of the graph structure to minimize lock duration std::unordered_map> localOutEdges; - + // Get the starting node and its edges { auto lock = lockGraph(LockIntent::Read); if (!lock || !lock->isLocked()) { throw LockAcquisitionException("Failed to acquire graph lock for DFS"); } - + auto nodeIt = nodes_.find(startKey); if (nodeIt == nodes_.end()) { - return; // Start node doesn't exist + return; // Start node doesn't exist } - + auto edgeIt = outEdges_.find(startKey); if (edgeIt != outEdges_.end()) { localOutEdges[startKey] = edgeIt->second; } } - + // Set up DFS std::unordered_set visited; std::stack stack; - + // Start with the initial node stack.push(startKey); - + // DFS main loop while (!stack.empty()) { KeyType current = stack.top(); stack.pop(); - + if (visited.find(current) != visited.end()) { - continue; // Skip already visited nodes + continue; // Skip already visited nodes } - + visited.insert(current); - + // Visit the node auto nodeLock = tryLockNode(current, LockIntent::Read, false, 50); if (nodeLock && nodeLock->isLocked()) { @@ -1435,14 +1340,14 @@ public: visitFunc(current, node->getData()); } } - + // Get the neighbors if we don't already have them locally if (localOutEdges.find(current) == localOutEdges.end()) { auto lock = lockGraph(LockIntent::Read); if (!lock || !lock->isLocked()) { - continue; // Skip if we can't get a lock + continue; // Skip if we can't get a lock } - + auto edgeIt = outEdges_.find(current); if (edgeIt != outEdges_.end()) { localOutEdges[current] = edgeIt->second; @@ -1450,7 +1355,7 @@ public: localOutEdges[current] = {}; } } - + // Push neighbors onto the stack in reverse order to maintain DFS order std::vector neighbors(localOutEdges[current].begin(), localOutEdges[current].end()); for (auto it = neighbors.rbegin(); it != neighbors.rend(); ++it) { @@ -1463,7 +1368,7 @@ public: /** * @brief Get all node keys in the graph - * + * * @return Vector of all node keys * @throws LockAcquisitionException If the graph lock cannot be acquired */ @@ -1472,20 +1377,20 @@ public: if (!lock || !lock->isLocked()) { throw LockAcquisitionException("Failed to acquire graph lock for getting all nodes"); } - + std::vector keys; keys.reserve(nodes_.size()); - + for (const auto& node : nodes_) { keys.push_back(node.first); } - + return keys; } /** * @brief Get the number of nodes in the graph - * + * * @return Node count * @throws LockAcquisitionException If the graph lock cannot be acquired */ @@ -1494,13 +1399,13 @@ public: if (!lock || !lock->isLocked()) { throw LockAcquisitionException("Failed to acquire graph lock for getting size"); } - + return nodes_.size(); } /** * @brief Check if the graph is empty - * + * * @return true if the graph has no nodes, false otherwise * @throws LockAcquisitionException If the graph lock cannot be acquired */ @@ -1509,7 +1414,7 @@ public: if (!lock || !lock->isLocked()) { throw LockAcquisitionException("Failed to acquire graph lock for checking emptiness"); } - + return nodes_.empty(); } @@ -1521,14 +1426,14 @@ public: if (!lock || !lock->isLocked()) { throw LockAcquisitionException("Failed to acquire graph lock for clearing"); } - + // Notify all nodes that they're being removed for (const auto& [_, node] : nodes_) { if (node) { node->notifyLockHolders(LockStatus::Preempted); } } - + nodes_.clear(); outEdges_.clear(); inEdges_.clear(); @@ -1536,7 +1441,7 @@ public: /** * @brief Register a callback for when a node is removed - * + * * @param callback Function to call when a node is removed * @return A string ID that can be used to unregister the callback */ @@ -1549,7 +1454,7 @@ public: /** * @brief Unregister a node removal callback - * + * * @param id ID of the callback to remove * @return true if the callback was removed, false if not found */ @@ -1562,13 +1467,13 @@ public: } return false; } - + /** * @brief Enable or disable lock history tracking - * + * * When enabled, the graph will track the history of lock acquisitions and releases, * which can be useful for debugging but adds some overhead. - * + * * @param enabled Whether to enable lock history tracking */ void setLockHistoryEnabled(bool enabled) { @@ -1578,10 +1483,10 @@ public: /** * @brief Enable or disable deadlock detection - * + * * When enabled, the graph will check for potential deadlocks before attempting * to acquire locks. This has some performance overhead but prevents deadlocks. - * + * * @param enabled Whether to enable deadlock detection */ void setDeadlockDetectionEnabled(bool enabled) { @@ -1591,21 +1496,18 @@ public: /** * @brief Try to acquire a lock on a resource with timeout - * + * * This method attempts to acquire a lock on the specified resource in the * requested mode, with deadlock prevention through lock ordering. - * + * * @param resourceKey Resource identifier * @param mode Lock acquisition mode * @param timeoutMs Timeout in milliseconds (default: 100ms) * @return A resource lock handle or nullptr if acquisition failed * @throws DeadlockDetectedException if a potential deadlock is detected */ - std::unique_ptr tryLockResource( - const KeyType& resourceKey, - LockMode mode, - size_t timeoutMs = 100 - ) { + std::unique_ptr tryLockResource(const KeyType& resourceKey, LockMode mode, + size_t timeoutMs = 100) { // First check if the resource exists if (!hasNode(resourceKey)) { return nullptr; @@ -1617,16 +1519,15 @@ public: // Check if acquiring this lock would cause a deadlock if (deadlockDetectionEnabled_) { if (wouldCauseDeadlock(resourceKey, threadId)) { - throw DeadlockDetectedException( - "Acquiring lock on resource " + std::string(resourceKey) + - " would cause a deadlock"); + throw DeadlockDetectedException("Acquiring lock on resource " + std::string(resourceKey) + + " would cause a deadlock"); } } // Determine the appropriate LockIntent based on the mode LockIntent intent; bool forWrite = false; - + switch (mode) { case LockMode::Shared: intent = LockIntent::Read; @@ -1646,49 +1547,33 @@ public: // Mark this resource as being locked by this thread threadResourceMap_[threadId].insert(resourceKey); - + // Record the time of the lock attempt if (lockHistoryEnabled_) { - lockHistory_.push_back({ - "Attempt lock", - resourceKey, - threadId, - std::chrono::steady_clock::now(), - mode - }); - } - + lockHistory_.push_back({"Attempt lock", resourceKey, threadId, std::chrono::steady_clock::now(), mode}); + } + // Update lock status resourceLockStatus_[resourceKey][threadId] = ResourceLockStatus::Pending; } // Try to acquire the node lock through the base class - auto nodeLock = tryLockNode( - resourceKey, - intent, - forWrite, - timeoutMs, - nullptr // No callback needed + auto nodeLock = tryLockNode(resourceKey, intent, forWrite, timeoutMs, + nullptr // No callback needed ); // If we failed to acquire the lock, clean up and return nullptr if (!nodeLock || !nodeLock->isLocked()) { std::lock_guard lock(lockGraphMutex_); - + // Remove the pending lock from our tracking threadResourceMap_[threadId].erase(resourceKey); resourceLockStatus_[resourceKey].erase(threadId); - + if (lockHistoryEnabled_) { - lockHistory_.push_back({ - "Failed lock", - resourceKey, - threadId, - std::chrono::steady_clock::now(), - mode - }); - } - + lockHistory_.push_back({"Failed lock", resourceKey, threadId, std::chrono::steady_clock::now(), mode}); + } + return nullptr; } @@ -1711,148 +1596,116 @@ public: std::lock_guard lock(lockGraphMutex_); resourceNodeLocks_[resourceKey][threadId] = std::move(nodeLock); resourceLockStatus_[resourceKey][threadId] = status; - + if (lockHistoryEnabled_) { - lockHistory_.push_back({ - "Acquired lock", - resourceKey, - threadId, - std::chrono::steady_clock::now(), - mode - }); + lockHistory_.push_back( + {"Acquired lock", resourceKey, threadId, std::chrono::steady_clock::now(), mode}); } } // Create and return the resource lock handle - return std::make_unique( - this, - resourceKey, - mode, - status, - threadId - ); + return std::make_unique(this, resourceKey, mode, status, threadId); } /** * @brief Release a lock on a resource - * + * * @param resourceKey Resource identifier * @param mode Lock mode that was used * @param threadId Thread ID that owns the lock (defaults to current thread) * @return true if the lock was released, false otherwise */ - bool releaseResourceLock( - const KeyType& resourceKey, - LockMode mode, - std::thread::id threadId = std::this_thread::get_id() - ) { + bool releaseResourceLock(const KeyType& resourceKey, LockMode mode, + std::thread::id threadId = std::this_thread::get_id()) { std::lock_guard lock(lockGraphMutex_); - + // Check if this thread has a lock on this resource auto threadIt = resourceNodeLocks_.find(resourceKey); if (threadIt == resourceNodeLocks_.end()) { return false; } - + auto lockIt = threadIt->second.find(threadId); if (lockIt == threadIt->second.end()) { return false; } - + // Release the node lock lockIt->second.reset(); - + // Clean up our tracking threadIt->second.erase(lockIt); if (threadIt->second.empty()) { resourceNodeLocks_.erase(threadIt); } - + threadResourceMap_[threadId].erase(resourceKey); if (threadResourceMap_[threadId].empty()) { threadResourceMap_.erase(threadId); } - + resourceLockStatus_[resourceKey].erase(threadId); if (resourceLockStatus_[resourceKey].empty()) { resourceLockStatus_.erase(resourceKey); } - + if (lockHistoryEnabled_) { - lockHistory_.push_back({ - "Released lock", - resourceKey, - threadId, - std::chrono::steady_clock::now(), - mode - }); - } - + lockHistory_.push_back({"Released lock", resourceKey, threadId, std::chrono::steady_clock::now(), mode}); + } + return true; } /** * @brief Upgrade a lock from shared to exclusive mode - * + * * @param resourceKey Resource identifier * @param threadId Thread ID that owns the lock (defaults to current thread) * @param timeoutMs Timeout in milliseconds * @return true if the upgrade succeeded, false otherwise */ - bool upgradeResourceLock( - const KeyType& resourceKey, - std::thread::id threadId = std::this_thread::get_id(), - size_t timeoutMs = 100 - ) { + bool upgradeResourceLock(const KeyType& resourceKey, std::thread::id threadId = std::this_thread::get_id(), + size_t timeoutMs = 100) { // First, release the existing shared lock { std::lock_guard lock(lockGraphMutex_); - + auto statusIt = resourceLockStatus_.find(resourceKey); if (statusIt == resourceLockStatus_.end()) { return false; } - + auto threadStatusIt = statusIt->second.find(threadId); - if (threadStatusIt == statusIt->second.end() || - threadStatusIt->second != ResourceLockStatus::Shared) { + if (threadStatusIt == statusIt->second.end() || threadStatusIt->second != ResourceLockStatus::Shared) { return false; } - + // Check if there's a lock handle auto locksIt = resourceNodeLocks_.find(resourceKey); if (locksIt == resourceNodeLocks_.end()) { return false; } - + auto threadLockIt = locksIt->second.find(threadId); if (threadLockIt == locksIt->second.end()) { return false; } - + // Release the shared lock threadLockIt->second.reset(); } - + // Now try to acquire an exclusive lock - auto nodeLock = tryLockNode( - resourceKey, - LockIntent::NodeModify, - true, // forWrite - timeoutMs, - nullptr - ); - + auto nodeLock = tryLockNode(resourceKey, LockIntent::NodeModify, + true, // forWrite + timeoutMs, nullptr); + if (!nodeLock || !nodeLock->isLocked()) { // Failed to upgrade, try to reacquire the shared lock - auto sharedLock = tryLockNode( - resourceKey, - LockIntent::Read, - false, // forWrite - timeoutMs, - nullptr - ); - + auto sharedLock = tryLockNode(resourceKey, LockIntent::Read, + false, // forWrite + timeoutMs, nullptr); + std::lock_guard lock(lockGraphMutex_); if (sharedLock && sharedLock->isLocked()) { resourceNodeLocks_[resourceKey][threadId] = std::move(sharedLock); @@ -1861,102 +1714,89 @@ public: // We couldn't reacquire the shared lock, remove all tracking releaseResourceLock(resourceKey, LockMode::Upgrade, threadId); } - + return false; } - + // Successfully upgraded to exclusive std::lock_guard lock(lockGraphMutex_); resourceNodeLocks_[resourceKey][threadId] = std::move(nodeLock); resourceLockStatus_[resourceKey][threadId] = ResourceLockStatus::Exclusive; - + if (lockHistoryEnabled_) { - lockHistory_.push_back({ - "Upgraded lock", - resourceKey, - threadId, - std::chrono::steady_clock::now(), - LockMode::Exclusive - }); - } - + lockHistory_.push_back( + {"Upgraded lock", resourceKey, threadId, std::chrono::steady_clock::now(), LockMode::Exclusive}); + } + return true; } /** * @brief Check if a thread holds a lock on a resource - * + * * @param resourceKey Resource identifier * @param threadId Thread ID to check (defaults to current thread) * @return true if the thread holds a lock, false otherwise */ - bool hasLock( - const KeyType& resourceKey, - std::thread::id threadId = std::this_thread::get_id() - ) const { + bool hasLock(const KeyType& resourceKey, std::thread::id threadId = std::this_thread::get_id()) const { std::lock_guard lock(lockGraphMutex_); - + auto threadIt = threadResourceMap_.find(threadId); if (threadIt == threadResourceMap_.end()) { return false; } - + return threadIt->second.find(resourceKey) != threadIt->second.end(); } /** * @brief Get the lock status of a resource for a thread - * + * * @param resourceKey Resource identifier * @param threadId Thread ID to check (defaults to current thread) * @return The current lock status */ - ResourceLockStatus getLockStatus( - const KeyType& resourceKey, - std::thread::id threadId = std::this_thread::get_id() - ) const { + ResourceLockStatus getLockStatus(const KeyType& resourceKey, + std::thread::id threadId = std::this_thread::get_id()) const { std::lock_guard lock(lockGraphMutex_); - + auto statusIt = resourceLockStatus_.find(resourceKey); if (statusIt == resourceLockStatus_.end()) { return ResourceLockStatus::Unlocked; } - + auto threadStatusIt = statusIt->second.find(threadId); if (threadStatusIt == statusIt->second.end()) { return ResourceLockStatus::Unlocked; } - + return threadStatusIt->second; } /** * @brief Acquire multiple resource locks in a safe order - * + * * This method acquires locks on multiple resources in an order that * prevents deadlocks, using the graph structure to determine the order. - * + * * @param resources Vector of resources to lock * @param mode Lock mode for all resources * @param timeoutMs Timeout in milliseconds per resource * @return Vector of lock handles, or empty vector if any acquisition failed */ - std::vector> tryLockResourcesInOrder( - const std::vector& resources, - LockMode mode, - size_t timeoutMs = 100 - ) { + std::vector> tryLockResourcesInOrder(const std::vector& resources, + LockMode mode, size_t timeoutMs = 100) { // If the resources are empty, return empty result if (resources.empty()) { return {}; } - + // Create a local subgraph with only the resources we care about auto subgraph = buildResourceLockSubgraph(resources); - + // Use topological sort to get a safe locking order std::vector lockOrder; - + // First try to use the built-in DAG sort if the resources form a sub-DAG auto topoOrder = getTopologicalOrderForResources(subgraph); if (!topoOrder.empty()) { @@ -1967,11 +1807,11 @@ public: lockOrder = resources; std::sort(lockOrder.begin(), lockOrder.end()); } - + // Acquire the locks in order std::vector> lockHandles; lockHandles.reserve(lockOrder.size()); - + for (const auto& resource : lockOrder) { auto lock = tryLockResource(resource, mode, timeoutMs); if (!lock || !lock->isLocked()) { @@ -1983,16 +1823,16 @@ public: } lockHandles.push_back(std::move(lock)); } - + return lockHandles; } /** * @brief Get the lock history - * + * * @return Vector of lock history entries */ - std::vector> + std::vector> getLockHistory() const { std::lock_guard lock(lockGraphMutex_); return lockHistory_; @@ -2006,30 +1846,30 @@ public: lockHistory_.clear(); } -private: + private: friend class GraphLockHandle; friend class NodeLockHandle; friend class ResourceLockHandle; - + // We've inlined the Node::tryLock method directly above - + /** * @brief Called when a graph lock is released - * + * * @param intent Intent of the lock that was released */ void onGraphLockReleased(LockIntent intent) { if (intent == LockIntent::GraphStructure) { currentStructuralIntent_.store(-1, std::memory_order_release); - + // Notify all nodes that the structural operation is complete notifyAllNodeLockHolders(LockStatus::Acquired); } } - + /** * @brief Notify all node lock holders about a status change - * + * * @param status New status to notify */ void notifyAllNodeLockHolders(LockStatus status) { @@ -2040,26 +1880,25 @@ private: } } } - + /** * @brief Check if a lock intent can proceed given the current state - * + * * @param intent Intent to check * @return true if the intent can proceed, false otherwise */ bool canProceedWithIntent(LockIntent intent) const { int current = currentStructuralIntent_.load(std::memory_order_acquire); - if (current >= 0 && - static_cast(current) == LockIntent::GraphStructure && + if (current >= 0 && static_cast(current) == LockIntent::GraphStructure && intent != LockIntent::Read) { return false; } return true; } - + /** * @brief Called when a node is removed from the graph - * + * * @param key Key of the removed node */ void onNodeRemoved(const KeyType& key) { @@ -2073,14 +1912,14 @@ private: /** * @brief Internal helper method for cycle detection - * + * * @param key Current node key * @param visited Map of visited nodes and their states * @return true if a cycle was detected, false otherwise */ bool hasCycleInternal(const KeyType& key, std::unordered_map& visited) const { visited[key] = NodeState::Visiting; - + // Safe access to outEdges auto it = outEdges_.find(key); if (it == outEdges_.end()) { @@ -2088,42 +1927,39 @@ private: visited[key] = NodeState::Visited; return false; } - + const auto& neighbors = it->second; for (const auto& neighbor : neighbors) { // Check if the neighbor exists in the nodes map if (nodes_.find(neighbor) == nodes_.end()) { - continue; // Skip non-existent nodes + continue; // Skip non-existent nodes } - + if (visited.find(neighbor) == visited.end()) { if (hasCycleInternal(neighbor, visited)) { return true; } } else if (visited[neighbor] == NodeState::Visiting) { - return true; // Cycle detected + return true; // Cycle detected } } - + visited[key] = NodeState::Visited; return false; } - + /** * @brief Check if acquiring a lock would cause a deadlock - * + * * This method checks both the DAG structure and thread dependencies to determine * if acquiring a lock would cause a deadlock. - * + * * @param resourceKey Resource identifier * @param threadId Thread ID that wants to acquire the lock * @return true if a deadlock would occur, false otherwise * @throws LockAcquisitionException If a required lock cannot be acquired */ - bool wouldCauseDeadlock( - const KeyType& resourceKey, - std::thread::id threadId - ) { + bool wouldCauseDeadlock(const KeyType& resourceKey, std::thread::id threadId) { // Lock ordering: lockGraphMutex_ first, then graphMutex_ (shared). // This matches tryLockResource() ordering, preventing ABBA deadlock. @@ -2186,8 +2022,7 @@ private: auto outEdgesIt = localOutEdges.find(current); if (outEdgesIt != localOutEdges.end()) { for (const auto& nextNode : outEdgesIt->second) { - if (visited.find(nextNode) == visited.end()) { - visited.insert(nextNode); + if (visited.insert(nextNode).second) { bfsQueue.push(nextNode); } } @@ -2200,26 +2035,25 @@ private: /** * @brief Build a subgraph of the lock dependencies between a set of resources - * + * * @param resources Vector of resources to include * @return Map of resources to their dependencies */ - std::unordered_map> buildResourceLockSubgraph( - const std::vector& resources - ) { + std::unordered_map> + buildResourceLockSubgraph(const std::vector& resources) { std::unordered_map> subgraph; - + // Create a set for faster lookups std::unordered_set resourceSet(resources.begin(), resources.end()); - + // For each resource, find its edges within the subset for (const auto& resource : resources) { // Add the resource to the subgraph subgraph[resource] = {}; - + // Get the outgoing edges auto outEdges = getOutEdges(resource); - + // Filter to only include edges to resources in our subset for (const auto& target : outEdges) { if (resourceSet.find(target) != resourceSet.end()) { @@ -2227,35 +2061,34 @@ private: } } } - + return subgraph; } /** * @brief Get a topological ordering of resources - * + * * @param subgraph Map of resources to their dependencies * @return Vector of resources in topological order, or empty if a cycle is detected */ - std::vector getTopologicalOrderForResources( - const std::unordered_map>& subgraph - ) { + std::vector + getTopologicalOrderForResources(const std::unordered_map>& subgraph) { std::vector result; std::unordered_map visited; std::unordered_map inProcess; - + // Helper function for DFS topological sort std::function visit = [&](const KeyType& key) { if (inProcess[key]) { - return false; // Cycle detected + return false; // Cycle detected } - + if (visited[key]) { return true; } - + inProcess[key] = true; - + auto it = subgraph.find(key); if (it != subgraph.end()) { for (const auto& neighbor : it->second) { @@ -2264,23 +2097,23 @@ private: } } } - + inProcess[key] = false; visited[key] = true; result.push_back(key); - + return true; }; - + // Visit all nodes for (const auto& [key, _] : subgraph) { if (!visited[key]) { if (!visit(key)) { - return {}; // Cycle detected + return {}; // Cycle detected } } } - + // Reverse to get the topological order std::reverse(result.begin(), result.end()); return result; @@ -2290,24 +2123,26 @@ private: std::unordered_map> nodes_; std::unordered_map> outEdges_; std::unordered_map> inEdges_; - + // Callbacks for node removal notification std::mutex callbackMutex_; std::unordered_map> removalCallbacks_; std::atomic callbackCounter_{0}; - + // Atomic flag for current structural intent. -1 = no intent, otherwise // stores static_cast(LockIntent). Lock-free reads in canProceedWithIntent. std::atomic currentStructuralIntent_{-1}; - + // Lock tracking state for DAG functionality mutable std::mutex lockGraphMutex_; - std::unordered_map>> resourceNodeLocks_; + std::unordered_map>> + resourceNodeLocks_; std::unordered_map> threadResourceMap_; std::unordered_map> resourceLockStatus_; - std::vector> lockHistory_; + std::vector> + lockHistory_; bool lockHistoryEnabled_ = false; bool deadlockDetectionEnabled_ = true; }; -} // namespace fabric \ No newline at end of file +} // namespace fabric diff --git a/include/fabric/utils/ErrorHandling.hh b/include/fabric/utils/ErrorHandling.hh index 0adb7c5b..76fb8a80 100644 --- a/include/fabric/utils/ErrorHandling.hh +++ b/include/fabric/utils/ErrorHandling.hh @@ -13,109 +13,100 @@ namespace fabric { * @brief Custom exception class for Fabric Engine errors */ class FabricException : public std::exception { -public: - explicit FabricException(const std::string &message); - const char *what() const noexcept override; + public: + explicit FabricException(const std::string& message); + const char* what() const noexcept override; -private: - std::string message; + private: + std::string message; }; -[[noreturn]] void throwError(const std::string &message); +[[noreturn]] void throwError(const std::string& message); // Lightweight error code for hot paths where exceptions are too expensive -enum class ErrorCode : uint16_t { - Ok = 0, - BufferOverrun, - InvalidState, - Timeout, - ConnectionReset, - PermissionDenied, - NotFound, - AlreadyExists, - ResourceExhausted, - Internal +enum class ErrorCode : std::uint8_t { + Ok = 0, + BufferOverrun, + InvalidState, + Timeout, + ConnectionReset, + PermissionDenied, + NotFound, + AlreadyExists, + ResourceExhausted, + Internal }; std::string_view errorCodeToString(ErrorCode code); // Result type combining value + error. Move-only. -template -class Result { -public: - static Result ok(T value) { return Result(std::move(value)); } - - static Result error(ErrorCode code, std::string message = "") { - return Result(code, std::move(message)); - } - - bool isOk() const { return code_ == ErrorCode::Ok; } - bool isError() const { return code_ != ErrorCode::Ok; } - ErrorCode code() const { return code_; } - const std::string &message() const { return message_; } - - T &value() { - if (isError()) - throwError("Result contains error: " + message_); - return *value_; - } - - const T &value() const { - if (isError()) - throwError("Result contains error: " + message_); - return *value_; - } - - T valueOr(T defaultValue) const { - if (isOk()) - return *value_; - return defaultValue; - } - - Result(Result &&) = default; - Result &operator=(Result &&) = default; - Result(const Result &) = delete; - Result &operator=(const Result &) = delete; - -private: - explicit Result(T value) - : code_(ErrorCode::Ok), value_(std::move(value)) {} - - Result(ErrorCode code, std::string message) - : code_(code), message_(std::move(message)) {} - - ErrorCode code_; - std::string message_; - std::optional value_; +template class Result { + public: + static Result ok(T value) { return Result(std::move(value)); } + + static Result error(ErrorCode code, std::string message = "") { return Result(code, std::move(message)); } + + bool isOk() const { return code_ == ErrorCode::Ok; } + bool isError() const { return code_ != ErrorCode::Ok; } + ErrorCode code() const { return code_; } + const std::string& message() const { return message_; } + + T& value() { + if (isError()) + throwError("Result contains error: " + message_); + return *value_; + } + + const T& value() const { + if (isError()) + throwError("Result contains error: " + message_); + return *value_; + } + + T valueOr(T defaultValue) const { + if (isOk()) + return *value_; + return defaultValue; + } + + Result(Result&&) = default; + Result& operator=(Result&&) = default; + Result(const Result&) = delete; + Result& operator=(const Result&) = delete; + + private: + explicit Result(T value) : code_(ErrorCode::Ok), value_(std::move(value)) {} + + Result(ErrorCode code, std::string message) : code_(code), message_(std::move(message)) {} + + ErrorCode code_; + std::string message_; + std::optional value_; }; -template <> -class Result { -public: - static Result ok() { return Result(); } +template <> class Result { + public: + static Result ok() { return Result(); } - static Result error(ErrorCode code, std::string message = "") { - return Result(code, std::move(message)); - } + static Result error(ErrorCode code, std::string message = "") { return Result(code, std::move(message)); } - bool isOk() const { return code_ == ErrorCode::Ok; } - bool isError() const { return code_ != ErrorCode::Ok; } - ErrorCode code() const { return code_; } - const std::string &message() const { return message_; } + bool isOk() const { return code_ == ErrorCode::Ok; } + bool isError() const { return code_ != ErrorCode::Ok; } + ErrorCode code() const { return code_; } + const std::string& message() const { return message_; } - Result(Result &&) = default; - Result &operator=(Result &&) = default; - Result(const Result &) = delete; - Result &operator=(const Result &) = delete; + Result(Result&&) = default; + Result& operator=(Result&&) = default; + Result(const Result&) = delete; + Result& operator=(const Result&) = delete; -private: - Result() : code_(ErrorCode::Ok) {} + private: + Result() : code_(ErrorCode::Ok) {} - Result(ErrorCode code, std::string message) - : code_(code), message_(std::move(message)) {} + Result(ErrorCode code, std::string message) : code_(code), message_(std::move(message)) {} - ErrorCode code_; - std::string message_; + ErrorCode code_; + std::string message_; }; } // namespace fabric diff --git a/include/fabric/utils/ImmutableDAG.hh b/include/fabric/utils/ImmutableDAG.hh index fbf2d271..a31cc2ec 100644 --- a/include/fabric/utils/ImmutableDAG.hh +++ b/include/fabric/utils/ImmutableDAG.hh @@ -18,211 +18,210 @@ using NodeId = size_t; // No locking: the caller ensures single-writer access. // Designed for commit-DAG-like structures where nodes are appended // and never mutated, edges are added infrequently, and reads are frequent. -template -class ImmutableDAG { -public: - // Append a new node. Returns its ID. - NodeId addNode(NodeData data) { - NodeId id = nodes_.size(); - nodes_.push_back(Node{std::move(data), {}, {}}); - return id; - } - - // Add a directed edge from -> to. Throws if the edge would create a cycle. - void addEdge(NodeId from, NodeId to) { - validateId(from); - validateId(to); - if (from == to) { - throwError("ImmutableDAG: self-loop on node " + std::to_string(from)); +template class ImmutableDAG { + public: + // Append a new node. Returns its ID. + NodeId addNode(NodeData data) { + NodeId id = nodes_.size(); + nodes_.push_back(Node{std::move(data), {}, {}}); + return id; } - // Check if adding from->to would create a cycle. - // A cycle exists iff 'to' can already reach 'from'. - if (isReachableInternal(to, from)) { - throwError("ImmutableDAG: adding edge " + std::to_string(from) + - " -> " + std::to_string(to) + " would create a cycle"); - } - nodes_[from].children.push_back(to); - nodes_[to].parents.push_back(from); - ++edgeCount_; - } - - size_t nodeCount() const { return nodes_.size(); } - size_t edgeCount() const { return edgeCount_; } - - const NodeData& getData(NodeId id) const { - validateId(id); - return nodes_[id].data; - } - - std::vector getParents(NodeId id) const { - validateId(id); - return nodes_[id].parents; - } - - std::vector getChildren(NodeId id) const { - validateId(id); - return nodes_[id].children; - } - - // Breadth-first traversal from start. Visitor returns false to stop. - void bfs(NodeId start, std::function visitor) const { - validateId(start); - std::vector visited(nodes_.size(), false); - std::queue q; - q.push(start); - visited[start] = true; - while (!q.empty()) { - NodeId cur = q.front(); - q.pop(); - if (!visitor(cur)) - return; - for (NodeId child : nodes_[cur].children) { - if (!visited[child]) { - visited[child] = true; - q.push(child); + + // Add a directed edge from -> to. Throws if the edge would create a cycle. + void addEdge(NodeId from, NodeId to) { + validateId(from); + validateId(to); + if (from == to) { + throwError("ImmutableDAG: self-loop on node " + std::to_string(from)); + } + // Check if adding from->to would create a cycle. + // A cycle exists iff 'to' can already reach 'from'. + if (isReachableInternal(to, from)) { + throwError("ImmutableDAG: adding edge " + std::to_string(from) + " -> " + std::to_string(to) + + " would create a cycle"); } - } + nodes_[from].children.push_back(to); + nodes_[to].parents.push_back(from); + ++edgeCount_; + } + + size_t nodeCount() const { return nodes_.size(); } + size_t edgeCount() const { return edgeCount_; } + + const NodeData& getData(NodeId id) const { + validateId(id); + return nodes_[id].data; } - } - - // Depth-first traversal from start. Visitor returns false to stop. - void dfs(NodeId start, std::function visitor) const { - validateId(start); - std::vector visited(nodes_.size(), false); - std::stack s; - s.push(start); - while (!s.empty()) { - NodeId cur = s.top(); - s.pop(); - if (visited[cur]) - continue; - visited[cur] = true; - if (!visitor(cur)) - return; - // Push children in reverse so leftmost child is visited first - const auto& ch = nodes_[cur].children; - for (auto it = ch.rbegin(); it != ch.rend(); ++it) { - if (!visited[*it]) - s.push(*it); - } + + std::vector getParents(NodeId id) const { + validateId(id); + return nodes_[id].parents; + } + + std::vector getChildren(NodeId id) const { + validateId(id); + return nodes_[id].children; } - } - - // Kahn's algorithm: returns all nodes in topological order. - std::vector topologicalSort() const { - size_t n = nodes_.size(); - std::vector inDeg(n, 0); - for (size_t i = 0; i < n; ++i) { - for (NodeId child : nodes_[i].children) { - ++inDeg[child]; - } + + // Breadth-first traversal from start. Visitor returns false to stop. + void bfs(NodeId start, std::function visitor) const { + validateId(start); + std::vector visited(nodes_.size(), false); + std::queue q; + q.push(start); + visited[start] = true; + while (!q.empty()) { + NodeId cur = q.front(); + q.pop(); + if (!visitor(cur)) + return; + for (NodeId child : nodes_[cur].children) { + if (!visited[child]) { + visited[child] = true; + q.push(child); + } + } + } } - std::queue ready; - for (size_t i = 0; i < n; ++i) { - if (inDeg[i] == 0) - ready.push(i); + + // Depth-first traversal from start. Visitor returns false to stop. + void dfs(NodeId start, std::function visitor) const { + validateId(start); + std::vector visited(nodes_.size(), false); + std::stack s; + s.push(start); + while (!s.empty()) { + NodeId cur = s.top(); + s.pop(); + if (visited[cur]) + continue; + visited[cur] = true; + if (!visitor(cur)) + return; + // Push children in reverse so leftmost child is visited first + const auto& ch = nodes_[cur].children; + for (auto it = ch.rbegin(); it != ch.rend(); ++it) { + if (!visited[*it]) + s.push(*it); + } + } } - std::vector result; - result.reserve(n); - while (!ready.empty()) { - NodeId cur = ready.front(); - ready.pop(); - result.push_back(cur); - for (NodeId child : nodes_[cur].children) { - if (--inDeg[child] == 0) - ready.push(child); - } + + // Kahn's algorithm: returns all nodes in topological order. + std::vector topologicalSort() const { + size_t n = nodes_.size(); + std::vector inDeg(n, 0); + for (size_t i = 0; i < n; ++i) { + for (NodeId child : nodes_[i].children) { + ++inDeg[child]; + } + } + std::queue ready; + for (size_t i = 0; i < n; ++i) { + if (inDeg[i] == 0) + ready.push(i); + } + std::vector result; + result.reserve(n); + while (!ready.empty()) { + NodeId cur = ready.front(); + ready.pop(); + result.push_back(cur); + for (NodeId child : nodes_[cur].children) { + if (--inDeg[child] == 0) + ready.push(child); + } + } + return result; } - return result; - } - - // Lowest common ancestor of a and b, traversing parents. - // Uses the set-intersection approach: walk ancestors of a and b - // in BFS order, return the first node reachable from both. - std::optional lca(NodeId a, NodeId b) const { - validateId(a); - validateId(b); - if (a == b) - return a; - - // Collect all ancestors of a (inclusive) - std::unordered_set ancestorsA; - { - std::queue q; - q.push(a); - ancestorsA.insert(a); - while (!q.empty()) { - NodeId cur = q.front(); - q.pop(); - for (NodeId p : nodes_[cur].parents) { - if (ancestorsA.insert(p).second) - q.push(p); + + // Lowest common ancestor of a and b, traversing parents. + // Uses the set-intersection approach: walk ancestors of a and b + // in BFS order, return the first node reachable from both. + std::optional lca(NodeId a, NodeId b) const { + validateId(a); + validateId(b); + if (a == b) + return a; + + // Collect all ancestors of a (inclusive) + std::unordered_set ancestorsA; + { + std::queue q; + q.push(a); + ancestorsA.insert(a); + while (!q.empty()) { + NodeId cur = q.front(); + q.pop(); + for (NodeId p : nodes_[cur].parents) { + if (ancestorsA.insert(p).second) + q.push(p); + } + } } - } + + // BFS from b upward; first hit in ancestorsA is the LCA + std::queue q; + std::unordered_set visitedB; + q.push(b); + visitedB.insert(b); + while (!q.empty()) { + NodeId cur = q.front(); + q.pop(); + if (ancestorsA.contains(cur)) + return cur; + for (NodeId p : nodes_[cur].parents) { + if (visitedB.insert(p).second) + q.push(p); + } + } + return std::nullopt; } - // BFS from b upward; first hit in ancestorsA is the LCA - std::queue q; - std::unordered_set visitedB; - q.push(b); - visitedB.insert(b); - while (!q.empty()) { - NodeId cur = q.front(); - q.pop(); - if (ancestorsA.contains(cur)) - return cur; - for (NodeId p : nodes_[cur].parents) { - if (visitedB.insert(p).second) - q.push(p); - } + // Returns true if 'to' is reachable from 'from' via directed edges. + bool isReachable(NodeId from, NodeId to) const { + validateId(from); + validateId(to); + return isReachableInternal(from, to); } - return std::nullopt; - } - - // Returns true if 'to' is reachable from 'from' via directed edges. - bool isReachable(NodeId from, NodeId to) const { - validateId(from); - validateId(to); - return isReachableInternal(from, to); - } - -private: - struct Node { - NodeData data; - std::vector parents; - std::vector children; - }; - - void validateId(NodeId id) const { - if (id >= nodes_.size()) { - throwError("ImmutableDAG: invalid node ID " + std::to_string(id)); + + private: + struct Node { + NodeData data; + std::vector parents; + std::vector children; + }; + + void validateId(NodeId id) const { + if (id >= nodes_.size()) { + throwError("ImmutableDAG: invalid node ID " + std::to_string(id)); + } } - } - - bool isReachableInternal(NodeId from, NodeId to) const { - if (from == to) - return true; - std::vector visited(nodes_.size(), false); - std::queue q; - q.push(from); - visited[from] = true; - while (!q.empty()) { - NodeId cur = q.front(); - q.pop(); - for (NodeId child : nodes_[cur].children) { - if (child == to) - return true; - if (!visited[child]) { - visited[child] = true; - q.push(child); + + bool isReachableInternal(NodeId from, NodeId to) const { + if (from == to) + return true; + std::vector visited(nodes_.size(), false); + std::queue q; + q.push(from); + visited[from] = true; + while (!q.empty()) { + NodeId cur = q.front(); + q.pop(); + for (NodeId child : nodes_[cur].children) { + if (child == to) + return true; + if (!visited[child]) { + visited[child] = true; + q.push(child); + } + } } - } + return false; } - return false; - } - std::vector nodes_; - size_t edgeCount_ = 0; + std::vector nodes_; + size_t edgeCount_ = 0; }; } // namespace fabric diff --git a/include/fabric/utils/Profiler.hh b/include/fabric/utils/Profiler.hh index 4da368c0..af5cea8c 100644 --- a/include/fabric/utils/Profiler.hh +++ b/include/fabric/utils/Profiler.hh @@ -5,95 +5,94 @@ // Otherwise, they compile to nothing. #ifdef FABRIC_PROFILING_ENABLED - #include - #include +#include +#include - // --- Zone Profiling --- - #define FABRIC_ZONE_SCOPED ZoneScoped - #define FABRIC_ZONE_SCOPED_N(name) ZoneScopedN(name) - #define FABRIC_ZONE_SCOPED_C(color) ZoneScopedC(color) - #define FABRIC_ZONE_SCOPED_NC(name, c) ZoneScopedNC(name, c) - #define FABRIC_ZONE_TEXT(txt, len) ZoneText(txt, len) - #define FABRIC_ZONE_NAME(txt, len) ZoneName(txt, len) - #define FABRIC_ZONE_VALUE(val) ZoneValue(val) - #define FABRIC_ZONE_COLOR(color) ZoneColor(color) +// --- Zone Profiling --- +#define FABRIC_ZONE_SCOPED ZoneScoped +#define FABRIC_ZONE_SCOPED_N(name) ZoneScopedN(name) +#define FABRIC_ZONE_SCOPED_C(color) ZoneScopedC(color) +#define FABRIC_ZONE_SCOPED_NC(name, c) ZoneScopedNC(name, c) +#define FABRIC_ZONE_TEXT(txt, len) ZoneText(txt, len) +#define FABRIC_ZONE_NAME(txt, len) ZoneName(txt, len) +#define FABRIC_ZONE_VALUE(val) ZoneValue(val) +#define FABRIC_ZONE_COLOR(color) ZoneColor(color) - // With callstack capture (depth = number of frames to capture) - #define FABRIC_ZONE_SCOPED_S(depth) ZoneScopedS(depth) - #define FABRIC_ZONE_SCOPED_NS(name, depth) ZoneScopedNS(name, depth) +// With callstack capture (depth = number of frames to capture) +#define FABRIC_ZONE_SCOPED_S(depth) ZoneScopedS(depth) +#define FABRIC_ZONE_SCOPED_NS(name, depth) ZoneScopedNS(name, depth) - // --- Frame Marking --- - #define FABRIC_FRAME_MARK FrameMark - #define FABRIC_FRAME_MARK_NAMED(name) FrameMarkNamed(name) - #define FABRIC_FRAME_MARK_START(name) FrameMarkStart(name) - #define FABRIC_FRAME_MARK_END(name) FrameMarkEnd(name) +// --- Frame Marking --- +#define FABRIC_FRAME_MARK FrameMark +#define FABRIC_FRAME_MARK_NAMED(name) FrameMarkNamed(name) +#define FABRIC_FRAME_MARK_START(name) FrameMarkStart(name) +#define FABRIC_FRAME_MARK_END(name) FrameMarkEnd(name) - // --- Memory Profiling --- - #define FABRIC_ALLOC(ptr, size) TracyAlloc(ptr, size) - #define FABRIC_FREE(ptr) TracyFree(ptr) - #define FABRIC_ALLOC_N(ptr, size, name) TracyAllocN(ptr, size, name) - #define FABRIC_FREE_N(ptr, name) TracyFreeN(ptr, name) - #define FABRIC_ALLOC_S(ptr, size, depth) TracyAllocS(ptr, size, depth) - #define FABRIC_FREE_S(ptr, depth) TracyFreeS(ptr, depth) +// --- Memory Profiling --- +#define FABRIC_ALLOC(ptr, size) TracyAlloc(ptr, size) +#define FABRIC_FREE(ptr) TracyFree(ptr) +#define FABRIC_ALLOC_N(ptr, size, name) TracyAllocN(ptr, size, name) +#define FABRIC_FREE_N(ptr, name) TracyFreeN(ptr, name) +#define FABRIC_ALLOC_S(ptr, size, depth) TracyAllocS(ptr, size, depth) +#define FABRIC_FREE_S(ptr, depth) TracyFreeS(ptr, depth) - // --- Lock Profiling --- - #define FABRIC_LOCKABLE(type, var) TracyLockable(type, var) - #define FABRIC_LOCKABLE_N(type, var, desc) TracyLockableN(type, var, desc) - #define FABRIC_SHARED_LOCKABLE(type, var) TracySharedLockable(type, var) - #define FABRIC_SHARED_LOCKABLE_N(type, var, d) TracySharedLockableN(type, var, d) - #define FABRIC_LOCKABLE_BASE(type) LockableBase(type) - #define FABRIC_SHARED_LOCKABLE_BASE(type) SharedLockableBase(type) - #define FABRIC_LOCK_MARK(var) LockMark(var) +// --- Lock Profiling --- +#define FABRIC_LOCKABLE(type, var) TracyLockable(type, var) +#define FABRIC_LOCKABLE_N(type, var, desc) TracyLockableN(type, var, desc) +#define FABRIC_SHARED_LOCKABLE(type, var) TracySharedLockable(type, var) +#define FABRIC_SHARED_LOCKABLE_N(type, var, d) TracySharedLockableN(type, var, d) +#define FABRIC_LOCKABLE_BASE(type) LockableBase(type) +#define FABRIC_SHARED_LOCKABLE_BASE(type) SharedLockableBase(type) +#define FABRIC_LOCK_MARK(var) LockMark(var) - // --- Thread Naming --- - #define FABRIC_SET_THREAD_NAME(name) tracy::SetThreadName(name) +// --- Thread Naming --- +#define FABRIC_SET_THREAD_NAME(name) tracy::SetThreadName(name) - // --- Messages / Logging --- - #define FABRIC_MESSAGE(txt, len) TracyMessage(txt, len) - #define FABRIC_MESSAGE_L(txt) TracyMessageL(txt) +// --- Messages / Logging --- +#define FABRIC_MESSAGE(txt, len) TracyMessage(txt, len) +#define FABRIC_MESSAGE_L(txt) TracyMessageL(txt) - // --- Plots --- - #define FABRIC_PLOT(name, val) TracyPlot(name, val) - #define FABRIC_PLOT_CONFIG(name, type, step, fill, color) \ - TracyPlotConfig(name, type, step, fill, color) +// --- Plots --- +#define FABRIC_PLOT(name, val) TracyPlot(name, val) +#define FABRIC_PLOT_CONFIG(name, type, step, fill, color) TracyPlotConfig(name, type, step, fill, color) #else - // All macros compile to nothing when profiling is disabled. - #define FABRIC_ZONE_SCOPED - #define FABRIC_ZONE_SCOPED_N(name) - #define FABRIC_ZONE_SCOPED_C(color) - #define FABRIC_ZONE_SCOPED_NC(name, c) - #define FABRIC_ZONE_TEXT(txt, len) - #define FABRIC_ZONE_NAME(txt, len) - #define FABRIC_ZONE_VALUE(val) - #define FABRIC_ZONE_COLOR(color) - #define FABRIC_ZONE_SCOPED_S(depth) - #define FABRIC_ZONE_SCOPED_NS(name, depth) +// All macros compile to nothing when profiling is disabled. +#define FABRIC_ZONE_SCOPED +#define FABRIC_ZONE_SCOPED_N(name) +#define FABRIC_ZONE_SCOPED_C(color) +#define FABRIC_ZONE_SCOPED_NC(name, c) +#define FABRIC_ZONE_TEXT(txt, len) +#define FABRIC_ZONE_NAME(txt, len) +#define FABRIC_ZONE_VALUE(val) +#define FABRIC_ZONE_COLOR(color) +#define FABRIC_ZONE_SCOPED_S(depth) +#define FABRIC_ZONE_SCOPED_NS(name, depth) - #define FABRIC_FRAME_MARK - #define FABRIC_FRAME_MARK_NAMED(name) - #define FABRIC_FRAME_MARK_START(name) - #define FABRIC_FRAME_MARK_END(name) +#define FABRIC_FRAME_MARK +#define FABRIC_FRAME_MARK_NAMED(name) +#define FABRIC_FRAME_MARK_START(name) +#define FABRIC_FRAME_MARK_END(name) - #define FABRIC_ALLOC(ptr, size) - #define FABRIC_FREE(ptr) - #define FABRIC_ALLOC_N(ptr, size, name) - #define FABRIC_FREE_N(ptr, name) - #define FABRIC_ALLOC_S(ptr, size, depth) - #define FABRIC_FREE_S(ptr, depth) +#define FABRIC_ALLOC(ptr, size) +#define FABRIC_FREE(ptr) +#define FABRIC_ALLOC_N(ptr, size, name) +#define FABRIC_FREE_N(ptr, name) +#define FABRIC_ALLOC_S(ptr, size, depth) +#define FABRIC_FREE_S(ptr, depth) - // LOCKABLE no-ops must still declare the variable - #define FABRIC_LOCKABLE(type, var) type var - #define FABRIC_LOCKABLE_N(type, var, desc) type var - #define FABRIC_SHARED_LOCKABLE(type, var) type var - #define FABRIC_SHARED_LOCKABLE_N(type, var, d) type var - #define FABRIC_LOCKABLE_BASE(type) type - #define FABRIC_SHARED_LOCKABLE_BASE(type) type - #define FABRIC_LOCK_MARK(var) +// LOCKABLE no-ops must still declare the variable +#define FABRIC_LOCKABLE(type, var) type var +#define FABRIC_LOCKABLE_N(type, var, desc) type var +#define FABRIC_SHARED_LOCKABLE(type, var) type var +#define FABRIC_SHARED_LOCKABLE_N(type, var, d) type var +#define FABRIC_LOCKABLE_BASE(type) type +#define FABRIC_SHARED_LOCKABLE_BASE(type) type +#define FABRIC_LOCK_MARK(var) - #define FABRIC_SET_THREAD_NAME(name) - #define FABRIC_MESSAGE(txt, len) - #define FABRIC_MESSAGE_L(txt) - #define FABRIC_PLOT(name, val) - #define FABRIC_PLOT_CONFIG(name, type, step, fill, color) +#define FABRIC_SET_THREAD_NAME(name) +#define FABRIC_MESSAGE(txt, len) +#define FABRIC_MESSAGE_L(txt) +#define FABRIC_PLOT(name, val) +#define FABRIC_PLOT_CONFIG(name, type, step, fill, color) #endif diff --git a/include/fabric/utils/Testing.hh b/include/fabric/utils/Testing.hh index 713c7bdb..92f71c8f 100644 --- a/include/fabric/utils/Testing.hh +++ b/include/fabric/utils/Testing.hh @@ -3,16 +3,16 @@ #include "fabric/core/Component.hh" #include "fabric/core/Event.hh" #include "fabric/core/Lifecycle.hh" -#include -#include #include -#include -#include #include +#include #include -#include -#include +#include #include +#include +#include +#include +#include namespace fabric { namespace Testing { @@ -21,177 +21,179 @@ namespace Testing { * @brief Mock component for testing */ class MockComponent : public Component { -public: - explicit MockComponent(const std::string& id) : Component(id) {} - - void initialize() override { initialize_impl(); } - std::string render() override { render_impl(); return ""; } - void update(float deltaTime) override { update_impl(deltaTime); } - void cleanup() override { cleanup_impl(); } - - // Test helpers - int initializeCallCount = 0; - int renderCallCount = 0; - int updateCallCount = 0; - int cleanupCallCount = 0; - - // Override these methods for testing - void initialize_impl() { initializeCallCount++; } - std::string render_impl() { renderCallCount++; return ""; } - void update_impl(float deltaTime) { updateCallCount++; } - void cleanup_impl() { cleanupCallCount++; } + public: + explicit MockComponent(const std::string& id) : Component(id) {} + + void initialize() override { initialize_impl(); } + std::string render() override { + render_impl(); + return ""; + } + void update(float deltaTime) override { update_impl(deltaTime); } + void cleanup() override { cleanup_impl(); } + + // Test helpers + int initializeCallCount = 0; + int renderCallCount = 0; + int updateCallCount = 0; + int cleanupCallCount = 0; + + // Override these methods for testing + void initialize_impl() { initializeCallCount++; } + std::string render_impl() { + renderCallCount++; + return ""; + } + void update_impl(float deltaTime) { updateCallCount++; } + void cleanup_impl() { cleanupCallCount++; } }; /** * @brief Event recorder for testing event dispatch */ class EventRecorder { -public: - /** - * @brief Record an event - * - * @param event The event to record - */ - void recordEvent(const Event& event) { - lastEventType = event.getType(); - lastEventSource = event.getSource(); - eventCount++; - } - - /** - * @brief Get a handler function for the event recorder - * - * @return Event handler function - */ - EventHandler getHandler() { - return [this](const Event& event) { - this->recordEvent(event); - }; - } - - /** - * @brief Reset the recorder - */ - void reset() { - lastEventType = ""; - lastEventSource = ""; - eventCount = 0; - } - - // Record data - std::string lastEventType; - std::string lastEventSource; - int eventCount = 0; + public: + /** + * @brief Record an event + * + * @param event The event to record + */ + void recordEvent(const Event& event) { + lastEventType = event.getType(); + lastEventSource = event.getSource(); + eventCount++; + } + + /** + * @brief Get a handler function for the event recorder + * + * @return Event handler function + */ + EventHandler getHandler() { + return [this](const Event& event) { + this->recordEvent(event); + }; + } + + /** + * @brief Reset the recorder + */ + void reset() { + lastEventType = ""; + lastEventSource = ""; + eventCount = 0; + } + + // Record data + std::string lastEventType; + std::string lastEventSource; + int eventCount = 0; }; /** * @brief Lifecycle recorder for testing lifecycle transitions */ class LifecycleRecorder { -public: - /** - * @brief Record a lifecycle state change - * - * @param state The new state - */ - void recordState(LifecycleState state) { - lastState = state; - stateChanges++; - } - - /** - * @brief Get a handler function for the lifecycle recorder - * - * @return Lifecycle hook function - */ - LifecycleHook getHook() { - return [this]() { - this->stateChanges++; - }; - } - - /** - * @brief Get a transition handler function for the lifecycle recorder - * - * @param fromState The from state - * @param toState The to state - * @return Lifecycle hook function - */ - LifecycleHook getTransitionHook(LifecycleState fromState, LifecycleState toState) { - return [this, fromState, toState]() { - this->lastFromState = fromState; - this->lastToState = toState; - this->transitionChanges++; - }; - } - - /** - * @brief Reset the recorder - */ - void reset() { - lastState = LifecycleState::Created; - lastFromState = LifecycleState::Created; - lastToState = LifecycleState::Created; - stateChanges = 0; - transitionChanges = 0; - } - - // Record data - LifecycleState lastState = LifecycleState::Created; - LifecycleState lastFromState = LifecycleState::Created; - LifecycleState lastToState = LifecycleState::Created; - int stateChanges = 0; - int transitionChanges = 0; + public: + /** + * @brief Record a lifecycle state change + * + * @param state The new state + */ + void recordState(LifecycleState state) { + lastState = state; + stateChanges++; + } + + /** + * @brief Get a handler function for the lifecycle recorder + * + * @return Lifecycle hook function + */ + LifecycleHook getHook() { + return [this]() { + this->stateChanges++; + }; + } + + /** + * @brief Get a transition handler function for the lifecycle recorder + * + * @param fromState The from state + * @param toState The to state + * @return Lifecycle hook function + */ + LifecycleHook getTransitionHook(LifecycleState fromState, LifecycleState toState) { + return [this, fromState, toState]() { + this->lastFromState = fromState; + this->lastToState = toState; + this->transitionChanges++; + }; + } + + /** + * @brief Reset the recorder + */ + void reset() { + lastState = LifecycleState::Created; + lastFromState = LifecycleState::Created; + lastToState = LifecycleState::Created; + stateChanges = 0; + transitionChanges = 0; + } + + // Record data + LifecycleState lastState = LifecycleState::Created; + LifecycleState lastFromState = LifecycleState::Created; + LifecycleState lastToState = LifecycleState::Created; + int stateChanges = 0; + int transitionChanges = 0; }; /** * @brief Run a function with a timeout - * + * * @tparam Func Function type * @param func Function to run * @param timeout Timeout duration * @return true if the function completed before the timeout, false otherwise */ -template -bool RunWithTimeout(Func&& func, std::chrono::milliseconds timeout) { +template bool RunWithTimeout(Func&& func, std::chrono::milliseconds timeout) { std::atomic completed{false}; - + std::thread t([&]() { func(); completed = true; }); - + // Wait for completion or timeout auto start = std::chrono::steady_clock::now(); while (!completed) { auto now = std::chrono::steady_clock::now(); if (now - start > timeout) { - t.detach(); // Don't join the thread if it's still running + t.detach(); // Don't join the thread if it's still running return false; } std::this_thread::sleep_for(std::chrono::milliseconds(10)); } - + t.join(); return true; } /** * @brief Run a function with multiple concurrent threads - * + * * @param threadCount Number of threads to spawn * @param iterationsPerThread Number of iterations per thread * @param func Function to run in each iteration * @throws std::runtime_error if any thread throws an exception */ -inline void RunConcurrent( - size_t threadCount, - size_t iterationsPerThread, - std::function func -) { +inline void RunConcurrent(size_t threadCount, size_t iterationsPerThread, + std::function func) { std::vector> futures; futures.reserve(threadCount); - + for (size_t threadId = 0; threadId < threadCount; ++threadId) { futures.push_back(std::async(std::launch::async, [=]() { for (size_t i = 0; i < iterationsPerThread; ++i) { @@ -199,7 +201,7 @@ inline void RunConcurrent( } })); } - + // Wait for all threads to complete and propagate exceptions for (auto& future : futures) { future.get(); @@ -208,33 +210,32 @@ inline void RunConcurrent( /** * @brief Generate a random string of the specified length - * + * * @param length Length of the string to generate * @return Random string */ inline std::string RandomString(size_t length) { - static const char charset[] = - "0123456789" - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz"; - + static const char charset[] = "0123456789" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz"; + std::random_device rd; std::mt19937 generator(rd()); std::uniform_int_distribution distribution(0, sizeof(charset) - 2); - + std::string result; result.reserve(length); - + for (size_t i = 0; i < length; ++i) { result += charset[distribution(generator)]; } - + return result; } /** * @brief Generate a random integer within the specified range - * + * * @param min Minimum value (inclusive) * @param max Maximum value (inclusive) * @return Random integer @@ -247,4 +248,4 @@ inline int RandomInt(int min, int max) { } } // namespace Testing -} // namespace fabric \ No newline at end of file +} // namespace fabric diff --git a/include/fabric/utils/ThreadPoolExecutor.hh b/include/fabric/utils/ThreadPoolExecutor.hh index 39cdbad5..bf0a6f3c 100644 --- a/include/fabric/utils/ThreadPoolExecutor.hh +++ b/include/fabric/utils/ThreadPoolExecutor.hh @@ -1,19 +1,19 @@ #pragma once #include +#include #include #include #include +#include #include +#include #include +#include +#include #include -#include -#include #include -#include -#include -#include -#include +#include namespace fabric { namespace Utils { @@ -22,84 +22,81 @@ namespace Utils { * @brief Exception thrown when a thread pool operation times out */ class ThreadPoolTimeoutException : public std::runtime_error { -public: - explicit ThreadPoolTimeoutException(const std::string& message) - : std::runtime_error(message) {} + public: + explicit ThreadPoolTimeoutException(const std::string& message) : std::runtime_error(message) {} }; /** * @brief A thread pool for executing asynchronous tasks with optimal concurrency - * + * * This class provides a reusable thread pool implementation for background tasks * with proper handling of thread lifecycle, task scheduling, and graceful shutdown. * It also includes testing support for deterministic behavior in tests. */ class ThreadPoolExecutor { -public: + public: /** * @brief Construct a new ThreadPoolExecutor with the specified number of threads - * + * * @param threadCount Number of worker threads to create (defaults to hardware concurrency) */ explicit ThreadPoolExecutor(size_t threadCount = std::thread::hardware_concurrency()); - + /** * @brief Destructor that ensures proper thread cleanup */ ~ThreadPoolExecutor(); - + /** * @brief ThreadPoolExecutor is not copyable */ ThreadPoolExecutor(const ThreadPoolExecutor&) = delete; ThreadPoolExecutor& operator=(const ThreadPoolExecutor&) = delete; - + /** * @brief ThreadPoolExecutor is not movable. * Worker threads capture `this`; moving would create dangling pointers. */ ThreadPoolExecutor(ThreadPoolExecutor&&) = delete; ThreadPoolExecutor& operator=(ThreadPoolExecutor&&) = delete; - + /** * @brief Set the number of worker threads - * + * * @param count Number of worker threads (must be at least 1) * @throws std::invalid_argument if count is 0 */ void setThreadCount(size_t count); - + /** * @brief Get the current number of worker threads - * + * * @return Number of worker threads */ size_t getThreadCount() const; - + /** * @brief Submit a task for execution - * + * * @tparam Func Function type * @tparam Args Argument types * @param func Function to execute * @param args Arguments to pass to the function * @return Future for the function's result */ - template - auto submit(Func&& func, Args&&... args) - -> std::future> { + template + auto submit(Func&& func, Args&&... args) -> std::future> { using ReturnType = std::invoke_result_t; - + // Create a packaged task with the function and its arguments auto task = std::make_shared>( [f = std::forward(func), ... args = std::forward(args)]() mutable { return f(std::forward(args)...); - } - ); - + }); + // Get the future from the packaged task std::future result = task->get_future(); - + // Wrap the task in a void function for the task queue auto taskWrapper = [task]() { try { @@ -113,16 +110,16 @@ public: std::cerr << "Unknown exception in thread pool task" << std::endl; } }; - + // Add the task to the queue { std::unique_lock lock(queueMutex_); - + // Check if the pool is shut down if (shutdown_) { throw std::runtime_error("Cannot submit task to stopped ThreadPoolExecutor"); } - + // Check if the pool is paused for testing if (pausedForTesting_) { // If paused, run the task immediately in this thread @@ -130,23 +127,23 @@ public: taskWrapper(); return result; } - + // Add the task to the queue taskQueue_.emplace(std::move(taskWrapper)); } - + // Notify a worker thread queueCondition_.notify_one(); - + return result; } - + /** * @brief Submit a task with a timeout for execution - * + * * If the task doesn't complete within the specified timeout, the future will * be set to a ThreadPoolTimeoutException. - * + * * @tparam Func Function type * @tparam Args Argument types * @param timeout Maximum time to wait for the task to complete @@ -154,30 +151,28 @@ public: * @param args Arguments to pass to the function * @return Future for the function's result */ - template + template auto submitWithTimeout(std::chrono::milliseconds timeout, Func&& func, Args&&... args) - -> std::future> { + -> std::future> { using ReturnType = std::invoke_result_t; - + // Create a promise for the result auto promise = std::make_shared>(); std::future result = promise->get_future(); - + // Create a packaged task that will run with a timeout - auto task = [promise, timeout, f = std::forward(func), - ... args = std::forward(args)]() mutable { + auto task = [promise, timeout, f = std::forward(func), ... args = std::forward(args)]() mutable { try { // Create a future for the actual task - auto innerTask = std::async(std::launch::async, - [&f, &args...]() { return f(std::forward(args)...); }); - + auto innerTask = + std::async(std::launch::async, [&f, &args...]() { return f(std::forward(args)...); }); + // Wait for the future with a timeout auto status = innerTask.wait_for(timeout); - + if (status == std::future_status::timeout) { // Task timed out - promise->set_exception(std::make_exception_ptr( - ThreadPoolTimeoutException("Task timed out"))); + promise->set_exception(std::make_exception_ptr(ThreadPoolTimeoutException("Task timed out"))); } else { // Task completed, get the result try { @@ -197,13 +192,13 @@ public: promise->set_exception(std::current_exception()); } }; - + // Submit the task to the thread pool submit(std::move(task)); - + return result; } - + /** * @brief Shutdown the thread pool * @@ -220,64 +215,64 @@ public: * @return true if all threads were gracefully shutdown, false if timeout occurred */ bool shutdown(std::chrono::milliseconds timeout = std::chrono::milliseconds(1000)); - + /** * @brief Check if the thread pool is shut down - * + * * @return true if the thread pool is shut down */ bool isShutdown() const; - + /** * @brief Pause the thread pool for testing - * + * * When paused, new tasks are executed immediately in the submitting thread * rather than being queued for worker threads. This allows for deterministic * testing without actual threading. */ void pauseForTesting(); - + /** * @brief Resume the thread pool after testing - * + * * This method resumes normal operation after a call to pauseForTesting(). */ void resumeAfterTesting(); - + /** * @brief Check if the thread pool is paused for testing - * + * * @return true if the thread pool is paused */ bool isPausedForTesting() const; - + /** * @brief Get the number of tasks currently in the queue - * + * * @return Task count */ size_t getQueuedTaskCount() const; -private: + private: // Worker thread function void workerThread(); - + // Task type using Task = std::function; - + // Thread management std::vector workerThreads_; std::atomic threadCount_; - + // Task queue std::queue taskQueue_; mutable std::mutex queueMutex_; std::condition_variable queueCondition_; - + // State std::atomic shutdown_{false}; std::atomic pausedForTesting_{false}; }; } // namespace Utils -} // namespace fabric \ No newline at end of file +} // namespace fabric diff --git a/include/fabric/utils/TimeoutLock.hh b/include/fabric/utils/TimeoutLock.hh index c1c491cf..efb86525 100644 --- a/include/fabric/utils/TimeoutLock.hh +++ b/include/fabric/utils/TimeoutLock.hh @@ -10,33 +10,30 @@ namespace fabric { namespace Utils { /** - * @brief Utility for timeout-protected lock acquisition - * + * @brief Utility for timeout-protected lock acquisition + * * This class provides static methods for acquiring locks with timeout protection * to prevent deadlocks and ensure non-blocking behavior. - * + * * @tparam MutexType The type of mutex to lock (must support standard locking operations) */ -template -class TimeoutLock { -public: +template class TimeoutLock { + public: /** * @brief Try to acquire a shared (read) lock with timeout - * + * * @param mutex The mutex to lock * @param timeout Maximum time to wait for the lock * @return An optional containing the lock if successful, or empty if timeout occurred */ - static std::optional> tryLockShared( - MutexType& mutex, - std::chrono::milliseconds timeout = std::chrono::milliseconds(100) - ) { + static std::optional> + tryLockShared(MutexType& mutex, std::chrono::milliseconds timeout = std::chrono::milliseconds(100)) { // First try to acquire the lock without waiting std::shared_lock lock(mutex, std::try_to_lock); if (lock.owns_lock()) { return lock; } - + // If immediate acquisition failed, try with timeout auto start = std::chrono::steady_clock::now(); while (true) { @@ -45,36 +42,34 @@ public: if (lock.owns_lock()) { return lock; } - + // Check if we've exceeded the timeout auto now = std::chrono::steady_clock::now(); if (std::chrono::duration_cast(now - start) >= timeout) { // Return empty optional to indicate timeout return std::nullopt; } - + // Sleep briefly to avoid hammering the mutex std::this_thread::sleep_for(std::chrono::milliseconds(1)); } } - + /** * @brief Try to acquire an exclusive (write) lock with timeout - * + * * @param mutex The mutex to lock * @param timeout Maximum time to wait for the lock * @return An optional containing the lock if successful, or empty if timeout occurred */ - static std::optional> tryLockUnique( - MutexType& mutex, - std::chrono::milliseconds timeout = std::chrono::milliseconds(100) - ) { + static std::optional> + tryLockUnique(MutexType& mutex, std::chrono::milliseconds timeout = std::chrono::milliseconds(100)) { // First try to acquire the lock without waiting std::unique_lock lock(mutex, std::try_to_lock); if (lock.owns_lock()) { return lock; } - + // If immediate acquisition failed, try with timeout auto start = std::chrono::steady_clock::now(); while (true) { @@ -83,50 +78,47 @@ public: if (lock.owns_lock()) { return lock; } - + // Check if we've exceeded the timeout auto now = std::chrono::steady_clock::now(); if (std::chrono::duration_cast(now - start) >= timeout) { // Return empty optional to indicate timeout return std::nullopt; } - + // Sleep briefly to avoid hammering the mutex std::this_thread::sleep_for(std::chrono::milliseconds(1)); } } - + /** * @brief Try to upgrade a shared lock to an exclusive lock with timeout - * + * * Note: This method will release the shared lock and acquire a unique lock. * It is NOT an atomic operation and should be used with caution. - * + * * @param mutex The mutex to lock * @param sharedLock The existing shared lock to upgrade * @param timeout Maximum time to wait for the lock * @return An optional containing the upgraded lock if successful, or empty if timeout occurred */ - static std::optional> tryUpgradeLock( - MutexType& mutex, - std::shared_lock& sharedLock, - std::chrono::milliseconds timeout = std::chrono::milliseconds(100) - ) { + static std::optional> + tryUpgradeLock(MutexType& mutex, std::shared_lock& sharedLock, + std::chrono::milliseconds timeout = std::chrono::milliseconds(100)) { // Release the shared lock sharedLock.unlock(); - + // Try to acquire an exclusive lock auto uniqueLock = tryLockUnique(mutex, timeout); - + // If we couldn't get the exclusive lock, try to reacquire the shared lock if (!uniqueLock) { sharedLock = std::shared_lock(mutex); } - + return uniqueLock; } - }; } // namespace Utils -} // namespace fabric \ No newline at end of file +} // namespace fabric diff --git a/include/fabric/utils/Utils.hh b/include/fabric/utils/Utils.hh index 428e7143..510a9f56 100644 --- a/include/fabric/utils/Utils.hh +++ b/include/fabric/utils/Utils.hh @@ -5,9 +5,9 @@ namespace fabric { class Utils { -public: - // Thread-safe. Generates prefix + `length` random hex digits. - static std::string generateUniqueId(const std::string& prefix, int length = 8); + public: + // Thread-safe. Generates prefix + `length` random hex digits. + static std::string generateUniqueId(const std::string& prefix, int length = 8); }; } // namespace fabric diff --git a/mise.toml b/mise.toml index bf3f0a73..91c7aed1 100644 --- a/mise.toml +++ b/mise.toml @@ -1,7 +1,29 @@ [tools] cmake = "latest" +codeql = { version = "latest", bin_path = "codeql", platforms = """ +[linux-x64] +asset_pattern = "codeql-linux64.zip" + +[macos-arm64] +asset_pattern = "codeql-osx64.zip" + +[macos-x64] +asset_pattern = "codeql-osx64.zip" + +[windows-x64] +asset_pattern = "codeql-win64.zip" +""" } ninja = "latest" +# LLVM: Homebrew manages install (macOS ABI compatibility), +# mise scopes the environment per-project via [env] below. +[env] +_.path = ["/opt/homebrew/opt/llvm/bin"] +CC = "clang" +CXX = "clang++" +CPPFLAGS = "-I/opt/homebrew/opt/llvm/include" +LDFLAGS = "-L/opt/homebrew/opt/llvm/lib" + # # Build # @@ -18,20 +40,37 @@ run = "sh tasks/build.sh" [tasks."build:release"] alias = "br" description = "Configure and build (Release)" -run = "BUILD_TYPE=Release sh tasks/build.sh" +run = "BUILD_PRESET=dev-release sh tasks/build.sh" # -# Lint +# Lint & Format # +[tasks.format] +alias = "fmt" +description = "Check clang-format on source files" +run = "sh tasks/format.sh" + +[tasks."format:fix"] +description = "Auto-format source files" +run = "FORMAT_FIX=1 sh tasks/format.sh" + [tasks.lint] -description = "Run clang-tidy on source files" +description = "Run clang-tidy on all source files (slow)" run = "sh tasks/lint.sh" +[tasks."lint:changed"] +description = "Run clang-tidy on git-dirty files only (fast)" +run = "LINT_CHANGED=1 sh tasks/lint.sh" + [tasks."lint:fix"] alias = "fix" -description = "Run clang-tidy with auto-fix" -run = "LINT_FIX=1 sh tasks/lint.sh" +description = "Run clang-tidy with auto-fix (CAUTION: can break cross-file refs)" +run = "LINT_FIX=1 LINT_CHANGED=1 sh tasks/lint.sh" + +[tasks.cppcheck] +description = "Run cppcheck static analysis" +run = "sh tasks/cppcheck.sh" # # Test @@ -59,3 +98,24 @@ description = "Run unit tests with gtest filter" depends = ["build"] usage = 'arg "filter" help="gtest --gtest_filter value"' run = "TEST_FILTER=\"{{arg(name='filter')}}\" sh tasks/test.sh" + +# +# Analysis +# + +[tasks.sanitize] +description = "Build and test with ASan + UBSan" +run = "sh tasks/sanitize.sh" + +[tasks."sanitize:tsan"] +description = "Build and test with ThreadSanitizer" +run = "SANITIZE_PRESET=ci-tsan sh tasks/sanitize.sh" + +[tasks.coverage] +description = "Build with coverage and generate lcov report" +run = "sh tasks/coverage.sh" + +[tasks.codeql] +description = "Run CodeQL security analysis locally" +depends = ["build"] +run = "sh tasks/codeql.sh" diff --git a/renovate.json b/renovate.json new file mode 100644 index 00000000..224411dc --- /dev/null +++ b/renovate.json @@ -0,0 +1,85 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:recommended"], + "labels": ["dependencies"], + "prHourlyLimit": 4, + "prConcurrentLimit": 10, + "customManagers": [ + { + "customType": "regex", + "description": "CMake FetchContent GIT_TAG dependencies", + "fileMatch": [ + "(^|/)CMakeLists\\.txt$", + "(^|/)cmake/modules/Fabric.*\\.cmake$" + ], + "matchStrings": [ + "GIT_REPOSITORY\\s+https://github\\.com/(?[^/]+/[^/\\s]+?)(?:\\.git)?\\s+GIT_TAG\\s+(?[^\\s]+)" + ], + "datasourceTemplate": "github-tags", + "versioningTemplate": "semver" + }, + { + "customType": "regex", + "description": "nlohmann/json URL-based download", + "fileMatch": ["(^|/)cmake/modules/FabricNlohmannJson\\.cmake$"], + "matchStrings": [ + "URL\\s+https://github\\.com/(?nlohmann/json)/releases/download/(?v[^/]+)/" + ], + "datasourceTemplate": "github-releases", + "versioningTemplate": "semver" + }, + { + "customType": "regex", + "description": "Standalone Asio (non-semver tag format: asio-X-Y-Z)", + "fileMatch": ["(^|/)cmake/modules/FabricAsio\\.cmake$"], + "matchStrings": [ + "GIT_REPOSITORY\\s+https://github\\.com/(?chriskohlhoff/asio)\\.git\\s+GIT_TAG\\s+(?asio-[^\\s]+)" + ], + "datasourceTemplate": "github-tags", + "versioningTemplate": "regex:^asio-(?\\d+)-(?\\d+)-(?\\d+)$" + }, + { + "customType": "regex", + "description": "bgfx.cmake (non-semver tag format: vMAJOR.MINOR.PATCH-BUILD)", + "fileMatch": ["(^|/)cmake/modules/FabricBgfx\\.cmake$"], + "matchStrings": [ + "GIT_REPOSITORY\\s+https://github\\.com/(?bkaradzic/bgfx\\.cmake)\\.git\\s+GIT_TAG\\s+(?v[^\\s]+)" + ], + "datasourceTemplate": "github-tags", + "versioningTemplate": "regex:^v(?\\d+)\\.(?\\d+)\\.(?\\d+)(-(?\\d+))?$" + }, + { + "customType": "regex", + "description": "Freetype (non-semver tag format: VER-X-Y-Z)", + "fileMatch": ["(^|/)cmake/modules/FabricRmlUi\\.cmake$"], + "matchStrings": [ + "GIT_REPOSITORY\\s+https://github\\.com/(?freetype/freetype)\\.git\\s+GIT_TAG\\s+(?VER-[^\\s]+)" + ], + "datasourceTemplate": "github-tags", + "versioningTemplate": "regex:^VER-(?\\d+)-(?\\d+)-(?\\d+)$" + } + ], + "packageRules": [ + { + "description": "Group all CMake FetchContent dependency updates", + "matchManagers": ["custom.regex"], + "groupName": "cmake-dependencies", + "groupSlug": "cmake-deps" + }, + { + "description": "Asio uses non-semver tags; auto-merge patch updates", + "matchPackageNames": ["chriskohlhoff/asio"], + "automerge": false + }, + { + "description": "bgfx uses non-standard versioning; pin to manual review", + "matchPackageNames": ["bkaradzic/bgfx.cmake"], + "automerge": false + }, + { + "description": "SDL3 uses release-X.Y.Z tags", + "matchPackageNames": ["libsdl-org/SDL"], + "extractVersion": "^release-(?.*)$" + } + ] +} diff --git a/src/core/Async.cc b/src/core/Async.cc index 1c3f8ff6..23ec437f 100644 --- a/src/core/Async.cc +++ b/src/core/Async.cc @@ -9,40 +9,40 @@ static asio::io_context io_ctx; static std::optional> work_guard; asio::io_context& context() { - return io_ctx; + return io_ctx; } void init() { - work_guard.emplace(asio::make_work_guard(io_ctx)); - FABRIC_LOG_INFO("Async: subsystem initialized"); + work_guard.emplace(asio::make_work_guard(io_ctx)); + FABRIC_LOG_INFO("Async: subsystem initialized"); } void shutdown() { - FABRIC_LOG_INFO("Async: subsystem shutting down"); - work_guard.reset(); - io_ctx.run(); + FABRIC_LOG_INFO("Async: subsystem shutting down"); + work_guard.reset(); + io_ctx.run(); } void poll() { - io_ctx.poll(); - io_ctx.restart(); + io_ctx.poll(); + io_ctx.restart(); } void run() { - io_ctx.run(); - io_ctx.restart(); + io_ctx.run(); + io_ctx.restart(); } asio::strand makeStrand() { - return asio::make_strand(io_ctx); + return asio::make_strand(io_ctx); } asio::steady_timer makeTimer() { - return asio::steady_timer(io_ctx); + return asio::steady_timer(io_ctx); } asio::steady_timer makeTimer(std::chrono::steady_clock::duration duration) { - return asio::steady_timer(io_ctx, duration); + return asio::steady_timer(io_ctx, duration); } } // namespace fabric::async diff --git a/src/core/Camera.cc b/src/core/Camera.cc index 395b6d1b..ce16dab6 100644 --- a/src/core/Camera.cc +++ b/src/core/Camera.cc @@ -19,7 +19,8 @@ void Camera::setPerspective(float fovYDeg, float aspect, float nearPlane, float bx::mtxProj(projection_, fovYDeg, aspect, nearPlane, farPlane, homogeneousNdc); } -void Camera::setOrthographic(float left, float right, float bottom, float top, float nearPlane, float farPlane, bool homogeneousNdc) { +void Camera::setOrthographic(float left, float right, float bottom, float top, float nearPlane, float farPlane, + bool homogeneousNdc) { near_ = nearPlane; far_ = farPlane; orthographic_ = true; @@ -30,10 +31,8 @@ void Camera::updateView(const Transform& transform) { auto pos = transform.getPosition(); // Left-handed: forward is +Z - auto fwd = transform.getRotation().rotateVector( - Vector3(0.0f, 0.0f, 1.0f)); - auto up = transform.getRotation().rotateVector( - Vector3(0.0f, 1.0f, 0.0f)); + auto fwd = transform.getRotation().rotateVector(Vector3(0.0f, 0.0f, 1.0f)); + auto up = transform.getRotation().rotateVector(Vector3(0.0f, 1.0f, 0.0f)); bx::Vec3 eye(pos.x, pos.y, pos.z); bx::Vec3 at(pos.x + fwd.x, pos.y + fwd.y, pos.z + fwd.z); @@ -54,10 +53,20 @@ void Camera::getViewProjection(float* outVP) const { bx::mtxMul(outVP, view_, projection_); } -float Camera::fovY() const { return fovY_; } -float Camera::aspectRatio() const { return aspect_; } -float Camera::nearPlane() const { return near_; } -float Camera::farPlane() const { return far_; } -bool Camera::isOrthographic() const { return orthographic_; } +float Camera::fovY() const { + return fovY_; +} +float Camera::aspectRatio() const { + return aspect_; +} +float Camera::nearPlane() const { + return near_; +} +float Camera::farPlane() const { + return far_; +} +bool Camera::isOrthographic() const { + return orthographic_; +} } // namespace fabric diff --git a/src/core/Component.cc b/src/core/Component.cc index ea69477f..a49844b6 100644 --- a/src/core/Component.cc +++ b/src/core/Component.cc @@ -1,123 +1,111 @@ #include "fabric/core/Component.hh" -#include "fabric/utils/ErrorHandling.hh" #include "fabric/core/Log.hh" +#include "fabric/utils/ErrorHandling.hh" #include namespace fabric { Component::Component(const std::string& id) : id(id) { - if (id.empty()) { - throwError("Component ID cannot be empty"); - } + if (id.empty()) { + throwError("Component ID cannot be empty"); + } } const std::string& Component::getId() const { - return id; + return id; } bool Component::hasProperty(const std::string& name) const { - std::lock_guard lock(propertiesMutex); - return properties.find(name) != properties.end(); + std::lock_guard lock(propertiesMutex); + return properties.find(name) != properties.end(); } bool Component::removeProperty(const std::string& name) { - std::lock_guard lock(propertiesMutex); - return properties.erase(name) > 0; + std::lock_guard lock(propertiesMutex); + return properties.erase(name) > 0; } void Component::addChild(std::shared_ptr child) { - if (!child) { - throwError("Cannot add null child to component"); - } - - std::lock_guard lock(childrenMutex); - - // Check for duplicate IDs - for (const auto& existingChild : children) { - if (existingChild->getId() == child->getId()) { - throwError("Child component with ID '" + child->getId() + "' already exists"); + if (!child) { + throwError("Cannot add null child to component"); + } + + std::lock_guard lock(childrenMutex); + + // Check for duplicate IDs + for (const auto& existingChild : children) { + if (existingChild->getId() == child->getId()) { + throwError("Child component with ID '" + child->getId() + "' already exists"); + } } - } - - children.push_back(child); - FABRIC_LOG_DEBUG("Added child '{}' to component '{}'", child->getId(), id); + + children.push_back(child); + FABRIC_LOG_DEBUG("Added child '{}' to component '{}'", child->getId(), id); } bool Component::removeChild(const std::string& childId) { - std::lock_guard lock(childrenMutex); - - auto it = std::find_if(children.begin(), children.end(), - [&childId](const auto& child) { return child->getId() == childId; }); - - if (it != children.end()) { - children.erase(it); - FABRIC_LOG_DEBUG("Removed child '{}' from component '{}'", childId, id); - return true; - } - - return false; + std::lock_guard lock(childrenMutex); + + auto it = std::find_if(children.begin(), children.end(), + [&childId](const auto& child) { return child->getId() == childId; }); + + if (it != children.end()) { + children.erase(it); + FABRIC_LOG_DEBUG("Removed child '{}' from component '{}'", childId, id); + return true; + } + + return false; } std::shared_ptr Component::getChild(const std::string& childId) const { - std::lock_guard lock(childrenMutex); - - auto it = std::find_if(children.begin(), children.end(), - [&childId](const auto& child) { return child->getId() == childId; }); - - if (it != children.end()) { - return *it; - } - - return nullptr; + std::lock_guard lock(childrenMutex); + + auto it = std::find_if(children.begin(), children.end(), + [&childId](const auto& child) { return child->getId() == childId; }); + + if (it != children.end()) { + return *it; + } + + return nullptr; } std::vector> Component::getChildren() const { - std::lock_guard lock(childrenMutex); - return children; + std::lock_guard lock(childrenMutex); + return children; } -template -void Component::setProperty(const std::string& name, const T& value) { - static_assert( - std::is_same_v || - std::is_same_v || - std::is_same_v || - std::is_same_v || - std::is_same_v || - std::is_same_v>, - "Property type not supported. Must be one of the types in PropertyValue." - ); - - std::lock_guard lock(propertiesMutex); - properties[name] = value; +template void Component::setProperty(const std::string& name, const T& value) { + static_assert(std::is_same_v || std::is_same_v || std::is_same_v || + std::is_same_v || std::is_same_v || + std::is_same_v>, + "Property type not supported. Must be one of the types in PropertyValue."); + + std::lock_guard lock(propertiesMutex); + properties[name] = value; } -template -T Component::getProperty(const std::string& name) const { - static_assert( - std::is_same_v || - std::is_same_v || - std::is_same_v || - std::is_same_v || - std::is_same_v || - std::is_same_v>, - "Property type not supported. Must be one of the types in PropertyValue." - ); - - std::lock_guard lock(propertiesMutex); - - auto it = properties.find(name); - if (it == properties.end()) { - throwError("Property '" + name + "' not found in component '" + id + "'"); - } - - try { - return std::get(it->second); - } catch (const std::bad_variant_access&) { - throwError("Property '" + name + "' has incorrect type"); - // This line is never reached due to throwError, but needed for compilation - return T(); - } +template T Component::getProperty(const std::string& name) const { + static_assert(std::is_same_v || std::is_same_v || std::is_same_v || + std::is_same_v || std::is_same_v || + std::is_same_v>, + "Property type not supported. Must be one of the types in PropertyValue."); + + std::lock_guard lock(propertiesMutex); + + auto it = properties.find(name); + if (it == properties.end()) { + throwError("Property '" + name + "' not found in component '" + id + "'"); + } + + try { + return std::get(it->second); + } catch (const std::bad_variant_access&) { + throwError("Property '" + name + "' has incorrect type"); + // This line is never reached due to throwError, but needed for compilation + return T(); + } } // Explicit template instantiations for common types @@ -135,4 +123,4 @@ template bool Component::getProperty(const std::string&) const; template std::string Component::getProperty(const std::string&) const; template std::shared_ptr Component::getProperty>(const std::string&) const; -} // namespace fabric \ No newline at end of file +} // namespace fabric diff --git a/src/core/ECS.cc b/src/core/ECS.cc index 1b603b0a..fb1fd4ff 100644 --- a/src/core/ECS.cc +++ b/src/core/ECS.cc @@ -54,12 +54,12 @@ void World::updateTransforms() { // CASCADE query ensures breadth-first order: parents are processed before children. // The optional ChildOf term means root entities (no parent) are also matched. auto q = world_->query_builder() - .with(flecs::ChildOf, flecs::Wildcard).cascade().optional() - .build(); + .with(flecs::ChildOf, flecs::Wildcard) + .cascade() + .optional() + .build(); - q.each([](flecs::entity e, - const Position& pos, const Rotation& rot, const Scale& scl, - LocalToWorld& ltw) { + q.each([](flecs::entity e, const Position& pos, const Rotation& rot, const Scale& scl, LocalToWorld& ltw) { // Compose local transform from Position * Rotation * Scale Transform t; t.setPosition(Vector3(pos.x, pos.y, pos.z)); @@ -70,8 +70,8 @@ void World::updateTransforms() { auto parent = e.parent(); if (parent.is_valid() && parent.has()) { // Parent already processed (CASCADE guarantee): multiply parent * local - const auto* parentLtw = parent.get(); - Matrix4x4 parentMat(parentLtw->matrix); + const auto& parentLtw = parent.get(); + Matrix4x4 parentMat(parentLtw.matrix); auto worldMatrix = parentMat * localMatrix; ltw.matrix = worldMatrix.elements; } else { @@ -82,8 +82,7 @@ void World::updateTransforms() { flecs::entity World::createSceneEntity(const char* name) { auto builder = name ? world_->entity(name) : world_->entity(); - return builder - .set({0.0f, 0.0f, 0.0f}) + return builder.set({0.0f, 0.0f, 0.0f}) .set({0.0f, 0.0f, 0.0f, 1.0f}) .set({1.0f, 1.0f, 1.0f}) .set({}) @@ -92,8 +91,7 @@ flecs::entity World::createSceneEntity(const char* name) { flecs::entity World::createChildEntity(flecs::entity parent, const char* name) { auto builder = name ? world_->entity(name) : world_->entity(); - return builder - .child_of(parent) + return builder.child_of(parent) .set({0.0f, 0.0f, 0.0f}) .set({0.0f, 0.0f, 0.0f, 1.0f}) .set({1.0f, 1.0f, 1.0f}) diff --git a/src/core/Event.cc b/src/core/Event.cc index d91292b8..10e117d3 100644 --- a/src/core/Event.cc +++ b/src/core/Event.cc @@ -1,94 +1,81 @@ #include "fabric/core/Event.hh" -#include "fabric/utils/ErrorHandling.hh" #include "fabric/core/Log.hh" +#include "fabric/utils/ErrorHandling.hh" #include "fabric/utils/Utils.hh" #include #include namespace fabric { -Event::Event(const std::string& type, const std::string& source) - : type(type), source(source) { - if (type.empty()) { - throwError("Event type cannot be empty"); - } +Event::Event(const std::string& type, const std::string& source) : type(type), source(source) { + if (type.empty()) { + throwError("Event type cannot be empty"); + } } const std::string& Event::getType() const { - return type; + return type; } const std::string& Event::getSource() const { - return source; + return source; } bool Event::hasData(const std::string& key) const { - std::lock_guard lock(dataMutex); - return data.find(key) != data.end(); + std::lock_guard lock(dataMutex); + return data.find(key) != data.end(); } -template -void Event::setData(const std::string& key, const T& value) { - static_assert( - std::is_same_v || - std::is_same_v || - std::is_same_v || - std::is_same_v || - std::is_same_v || - std::is_same_v>, - "Data type not supported. Must be one of the types in DataValue." - ); - - std::lock_guard lock(dataMutex); - data[key] = value; +template void Event::setData(const std::string& key, const T& value) { + static_assert(std::is_same_v || std::is_same_v || std::is_same_v || + std::is_same_v || std::is_same_v || + std::is_same_v>, + "Data type not supported. Must be one of the types in DataValue."); + + std::lock_guard lock(dataMutex); + data[key] = value; } -template -T Event::getData(const std::string& key) const { - static_assert( - std::is_same_v || - std::is_same_v || - std::is_same_v || - std::is_same_v || - std::is_same_v || - std::is_same_v>, - "Data type not supported. Must be one of the types in DataValue." - ); - - std::lock_guard lock(dataMutex); - - auto it = data.find(key); - if (it == data.end()) { - throwError("Event data key '" + key + "' not found"); - } - - try { - return std::get(it->second); - } catch (const std::bad_variant_access&) { - throwError("Event data key '" + key + "' has incorrect type"); - return T(); - } +template T Event::getData(const std::string& key) const { + static_assert(std::is_same_v || std::is_same_v || std::is_same_v || + std::is_same_v || std::is_same_v || + std::is_same_v>, + "Data type not supported. Must be one of the types in DataValue."); + + std::lock_guard lock(dataMutex); + + auto it = data.find(key); + if (it == data.end()) { + throwError("Event data key '" + key + "' not found"); + } + + try { + return std::get(it->second); + } catch (const std::bad_variant_access&) { + throwError("Event data key '" + key + "' has incorrect type"); + return T(); + } } bool Event::hasAnyData(const std::string& key) const { - std::lock_guard lock(dataMutex); - return anyData.find(key) != anyData.end(); + std::lock_guard lock(dataMutex); + return anyData.find(key) != anyData.end(); } bool Event::isHandled() const { - return handled; + return handled; } void Event::setHandled(bool handled) { - this->handled = handled; + this->handled = handled; } bool Event::isCancelled() const { - return cancelled; + return cancelled; } void Event::setCancelled(bool cancelled) { - this->cancelled = cancelled; + this->cancelled = cancelled; } // Explicit template instantiations @@ -106,91 +93,87 @@ template bool Event::getData(const std::string&) const; template std::string Event::getData(const std::string&) const; template std::vector Event::getData>(const std::string&) const; -std::string EventDispatcher::addEventListener(const std::string& eventType, - const EventHandler& handler, +std::string EventDispatcher::addEventListener(const std::string& eventType, const EventHandler& handler, int32_t priority) { - if (eventType.empty()) { - throwError("Event type cannot be empty"); - } - - if (!handler) { - throwError("Event handler cannot be null"); - } - - std::lock_guard lock(listenersMutex); - - HandlerEntry entry; - entry.id = Utils::generateUniqueId("h_"); - entry.handler = handler; - entry.priority = priority; - - // Insert in priority-sorted order (lower priority first). - // upper_bound preserves insertion order for equal priorities. - auto& vec = listeners[eventType]; - auto pos = std::upper_bound(vec.begin(), vec.end(), entry, - [](const HandlerEntry& a, const HandlerEntry& b) { - return a.priority < b.priority; - }); - vec.insert(pos, entry); - - FABRIC_LOG_DEBUG("Added event listener for type '{}' with ID '{}' (priority {})", - eventType, entry.id, priority); - - return entry.id; -} + if (eventType.empty()) { + throwError("Event type cannot be empty"); + } -bool EventDispatcher::removeEventListener(const std::string& eventType, const std::string& handlerId) { - std::lock_guard lock(listenersMutex); + if (!handler) { + throwError("Event handler cannot be null"); + } - auto it = listeners.find(eventType); - if (it == listeners.end()) { - return false; - } + std::lock_guard lock(listenersMutex); - auto& handlers = it->second; - auto handlerIt = std::find_if(handlers.begin(), handlers.end(), - [&handlerId](const HandlerEntry& entry) { return entry.id == handlerId; }); + HandlerEntry entry; + entry.id = Utils::generateUniqueId("h_"); + entry.handler = handler; + entry.priority = priority; - if (handlerIt != handlers.end()) { - handlers.erase(handlerIt); - FABRIC_LOG_DEBUG("Removed event listener for type '{}' with ID '{}'", eventType, handlerId); - return true; - } + // Insert in priority-sorted order (lower priority first). + // upper_bound preserves insertion order for equal priorities. + auto& vec = listeners[eventType]; + auto pos = std::upper_bound(vec.begin(), vec.end(), entry, + [](const HandlerEntry& a, const HandlerEntry& b) { return a.priority < b.priority; }); + vec.insert(pos, entry); - return false; -} + FABRIC_LOG_DEBUG("Added event listener for type '{}' with ID '{}' (priority {})", eventType, entry.id, priority); -bool EventDispatcher::dispatchEvent(Event& event) { - std::vector handlersToInvoke; + return entry.id; +} - { +bool EventDispatcher::removeEventListener(const std::string& eventType, const std::string& handlerId) { std::lock_guard lock(listenersMutex); - auto it = listeners.find(event.getType()); + auto it = listeners.find(eventType); if (it == listeners.end()) { - return false; + return false; } - handlersToInvoke = it->second; - } + auto& handlers = it->second; + auto handlerIt = std::find_if(handlers.begin(), handlers.end(), + [&handlerId](const HandlerEntry& entry) { return entry.id == handlerId; }); - bool handled = false; + if (handlerIt != handlers.end()) { + handlers.erase(handlerIt); + FABRIC_LOG_DEBUG("Removed event listener for type '{}' with ID '{}'", eventType, handlerId); + return true; + } - for (const auto& entry : handlersToInvoke) { - try { - entry.handler(event); - if (event.isCancelled() || event.isHandled()) { - handled = true; - break; - } - } catch (const std::exception& e) { - FABRIC_LOG_ERROR("Exception in event handler: {}", e.what()); - } catch (...) { - FABRIC_LOG_ERROR("Unknown exception in event handler"); + return false; +} + +bool EventDispatcher::dispatchEvent(Event& event) { + std::vector handlersToInvoke; + + { + std::lock_guard lock(listenersMutex); + + auto it = listeners.find(event.getType()); + if (it == listeners.end()) { + return false; + } + + handlersToInvoke = it->second; + } + + bool handled = false; + + for (const auto& entry : handlersToInvoke) { + try { + entry.handler(event); + if (event.isCancelled() || event.isHandled()) { + handled = true; + break; + } + } catch (const std::exception& e) { + FABRIC_LOG_ERROR("Exception in event handler: {}", e.what()); + } catch (...) { + FABRIC_LOG_ERROR("Unknown exception in event handler"); + } } - } - return handled; + return handled; } } // namespace fabric diff --git a/src/core/Fabric.cc b/src/core/Fabric.cc index 3691baef..c78e0e2c 100644 --- a/src/core/Fabric.cc +++ b/src/core/Fabric.cc @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -33,25 +34,19 @@ bgfx::PlatformData getPlatformData(SDL_Window* window) { SDL_PropertiesID props = SDL_GetWindowProperties(window); #if defined(SDL_PLATFORM_WIN32) - pd.nwh = SDL_GetPointerProperty( - props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr); + pd.nwh = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr); #elif defined(SDL_PLATFORM_MACOS) - pd.nwh = SDL_GetPointerProperty( - props, SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, nullptr); + pd.nwh = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, nullptr); #elif defined(SDL_PLATFORM_LINUX) - void* wl = SDL_GetPointerProperty( - props, SDL_PROP_WINDOW_WAYLAND_SURFACE_POINTER, nullptr); + void* wl = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_SURFACE_POINTER, nullptr); if (wl) { - pd.ndt = SDL_GetPointerProperty( - props, SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER, nullptr); + pd.ndt = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER, nullptr); pd.nwh = wl; pd.type = bgfx::NativeWindowHandleType::Wayland; } else { - pd.ndt = SDL_GetPointerProperty( - props, SDL_PROP_WINDOW_X11_DISPLAY_POINTER, nullptr); - pd.nwh = reinterpret_cast(static_cast( - SDL_GetNumberProperty( - props, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0))); + pd.ndt = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_X11_DISPLAY_POINTER, nullptr); + pd.nwh = reinterpret_cast( + static_cast(SDL_GetNumberProperty(props, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0))); } #endif @@ -70,15 +65,13 @@ int main(int argc, char* argv[]) { argParser.parse(argc, argv); if (argParser.hasArgument("--version")) { - std::cout << fabric::APP_NAME << " version " << fabric::APP_VERSION - << std::endl; + std::cout << fabric::APP_NAME << " version " << fabric::APP_VERSION << std::endl; fabric::log::shutdown(); return 0; } if (argParser.hasArgument("--help")) { - std::cout << "Usage: " << fabric::APP_EXECUTABLE_NAME << " [options]" - << std::endl; + std::cout << "Usage: " << fabric::APP_EXECUTABLE_NAME << " [options]" << std::endl; std::cout << "Options:" << std::endl; std::cout << " --version Display version information" << std::endl; std::cout << " --help Display this help message" << std::endl; @@ -96,9 +89,8 @@ int main(int argc, char* argv[]) { constexpr int kWindowWidth = 1280; constexpr int kWindowHeight = 720; - SDL_Window* window = SDL_CreateWindow( - fabric::APP_NAME, kWindowWidth, kWindowHeight, - SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_RESIZABLE); + SDL_Window* window = SDL_CreateWindow(fabric::APP_NAME, kWindowWidth, kWindowHeight, + SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_RESIZABLE); if (!window) { FABRIC_LOG_CRITICAL("Window creation failed: {}", SDL_GetError()); @@ -129,13 +121,10 @@ int main(int argc, char* argv[]) { return 1; } - bgfx::setViewClear( - 0, BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH, 0x303030ff, 1.0f, 0); - bgfx::setViewRect( - 0, 0, 0, static_cast(pw), static_cast(ph)); + bgfx::setViewClear(0, BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH, 0x303030ff, 1.0f, 0); + bgfx::setViewRect(0, 0, 0, static_cast(pw), static_cast(ph)); - FABRIC_LOG_INFO("bgfx renderer: {}", - bgfx::getRendererName(bgfx::getRendererType())); + FABRIC_LOG_INFO("bgfx renderer: {}", bgfx::getRendererName(bgfx::getRendererType())); // RmlUi backend interfaces fabric::BgfxSystemInterface rmlSystem; @@ -146,9 +135,7 @@ int main(int argc, char* argv[]) { Rml::SetRenderInterface(&rmlRenderer); Rml::Initialise(); - Rml::Context* rmlContext = Rml::CreateContext( - "main", - Rml::Vector2i(pw, ph)); + Rml::Context* rmlContext = Rml::CreateContext("main", Rml::Vector2i(pw, ph)); FABRIC_LOG_INFO("RmlUi context created ({}x{})", pw, ph); @@ -185,14 +172,16 @@ int main(int argc, char* argv[]) { dispatcher.addEventListener("time_faster", [&timeline](fabric::Event&) { double scale = timeline.getGlobalTimeScale() + 0.25; - if (scale > 4.0) scale = 4.0; + if (scale > 4.0) + scale = 4.0; timeline.setGlobalTimeScale(scale); FABRIC_LOG_INFO("Time scale: {:.2f}", timeline.getGlobalTimeScale()); }); dispatcher.addEventListener("time_slower", [&timeline](fabric::Event&) { double scale = timeline.getGlobalTimeScale() - 0.25; - if (scale < 0.25) scale = 0.25; + if (scale < 0.25) + scale = 0.25; timeline.setGlobalTimeScale(scale); FABRIC_LOG_INFO("Time scale: {:.2f}", timeline.getGlobalTimeScale()); }); @@ -240,11 +229,11 @@ int main(int argc, char* argv[]) { FABRIC_ZONE_SCOPED_N("main_loop"); auto now = std::chrono::high_resolution_clock::now(); - double frameTime = - std::chrono::duration(now - lastTime).count(); + double frameTime = std::chrono::duration(now - lastTime).count(); lastTime = now; - if (frameTime > 0.25) frameTime = 0.25; + if (frameTime > 0.25) + frameTime = 0.25; accumulator += frameTime; SDL_Event event; @@ -258,12 +247,10 @@ int main(int argc, char* argv[]) { auto w = static_cast(event.window.data1); auto h = static_cast(event.window.data2); bgfx::reset(w, h, BGFX_RESET_VSYNC); - bgfx::setViewRect(0, 0, 0, - static_cast(w), static_cast(h)); + bgfx::setViewRect(0, 0, 0, static_cast(w), static_cast(h)); float newAspect = static_cast(w) / static_cast(h); camera.setPerspective(60.0f, newAspect, 0.1f, 1000.0f, homogeneousNdc); - rmlContext->SetDimensions(Rml::Vector2i( - static_cast(w), static_cast(h))); + rmlContext->SetDimensions(Rml::Vector2i(static_cast(w), static_cast(h))); } } @@ -272,8 +259,10 @@ int main(int argc, char* argv[]) { cameraPitch += inputManager.mouseDeltaY() * kMouseSensitivity; constexpr float kMaxPitch = 1.5f; // ~86 degrees - if (cameraPitch > kMaxPitch) cameraPitch = kMaxPitch; - if (cameraPitch < -kMaxPitch) cameraPitch = -kMaxPitch; + if (cameraPitch > kMaxPitch) + cameraPitch = kMaxPitch; + if (cameraPitch < -kMaxPitch) + cameraPitch = -kMaxPitch; // Build camera rotation from yaw (Y axis) then pitch (X axis) auto yawQ = fabric::Quaternion::fromAxisAngle( @@ -289,21 +278,25 @@ int main(int argc, char* argv[]) { // Derive direction vectors inside the fixed step so movement // stays consistent if rotation is ever updated per tick. - auto fwd = rotation.rotateVector( - fabric::Vector3(0.0f, 0.0f, 1.0f)); - auto right = rotation.rotateVector( - fabric::Vector3(1.0f, 0.0f, 0.0f)); + auto fwd = rotation.rotateVector(fabric::Vector3(0.0f, 0.0f, 1.0f)); + auto right = rotation.rotateVector(fabric::Vector3(1.0f, 0.0f, 0.0f)); auto up = fabric::Vector3(0.0f, 1.0f, 0.0f); float step = kMoveSpeed * static_cast(kFixedDt); auto pos = cameraTransform.getPosition(); - if (inputManager.isActionActive("move_forward")) pos = pos + fwd * step; - if (inputManager.isActionActive("move_backward")) pos = pos - fwd * step; - if (inputManager.isActionActive("move_right")) pos = pos + right * step; - if (inputManager.isActionActive("move_left")) pos = pos - right * step; - if (inputManager.isActionActive("move_up")) pos = pos + up * step; - if (inputManager.isActionActive("move_down")) pos = pos - up * step; + if (inputManager.isActionActive("move_forward")) + pos = pos + fwd * step; + if (inputManager.isActionActive("move_backward")) + pos = pos - fwd * step; + if (inputManager.isActionActive("move_right")) + pos = pos + right * step; + if (inputManager.isActionActive("move_left")) + pos = pos - right * step; + if (inputManager.isActionActive("move_up")) + pos = pos + up * step; + if (inputManager.isActionActive("move_down")) + pos = pos - up * step; cameraTransform.setPosition(pos); accumulator -= kFixedDt; @@ -320,8 +313,7 @@ int main(int argc, char* argv[]) { // RmlUi overlay on view 255 (after 3D scene, before frame flip) int curW, curH; SDL_GetWindowSizeInPixels(window, &curW, &curH); - rmlRenderer.beginFrame( - static_cast(curW), static_cast(curH)); + rmlRenderer.beginFrame(static_cast(curW), static_cast(curH)); rmlContext->Update(); rmlContext->Render(); diff --git a/src/core/InputManager.cc b/src/core/InputManager.cc index 5408bb9d..62481a34 100644 --- a/src/core/InputManager.cc +++ b/src/core/InputManager.cc @@ -5,15 +5,14 @@ namespace fabric { InputManager::InputManager() = default; -InputManager::InputManager(EventDispatcher& dispatcher) - : dispatcher_(&dispatcher) {} +InputManager::InputManager(EventDispatcher& dispatcher) : dispatcher_(&dispatcher) {} void InputManager::bindKey(const std::string& action, SDL_Keycode key) { keyBindings_[key] = action; } void InputManager::unbindKey(const std::string& action) { - for (auto it = keyBindings_.begin(); it != keyBindings_.end(); ) { + for (auto it = keyBindings_.begin(); it != keyBindings_.end();) { if (it->second == action) { it = keyBindings_.erase(it); } else { @@ -24,69 +23,80 @@ void InputManager::unbindKey(const std::string& action) { bool InputManager::processEvent(const SDL_Event& event) { switch (event.type) { - case SDL_EVENT_KEY_DOWN: { - if (event.key.repeat) return false; - - auto it = keyBindings_.find(event.key.key); - if (it == keyBindings_.end()) return false; - - const auto& action = it->second; - activeActions_.insert(action); - - if (dispatcher_) { - Event e(action, "InputManager"); - dispatcher_->dispatchEvent(e); + case SDL_EVENT_KEY_DOWN: { + if (event.key.repeat) + return false; + + auto it = keyBindings_.find(event.key.key); + if (it == keyBindings_.end()) + return false; + + const auto& action = it->second; + activeActions_.insert(action); + + if (dispatcher_) { + Event e(action, "InputManager"); + dispatcher_->dispatchEvent(e); + } + return true; } - return true; - } - case SDL_EVENT_KEY_UP: { - auto it = keyBindings_.find(event.key.key); - if (it == keyBindings_.end()) return false; + case SDL_EVENT_KEY_UP: { + auto it = keyBindings_.find(event.key.key); + if (it == keyBindings_.end()) + return false; - const auto& action = it->second; - activeActions_.erase(action); + const auto& action = it->second; + activeActions_.erase(action); - if (dispatcher_) { - Event e(action + ":released", "InputManager"); - dispatcher_->dispatchEvent(e); + if (dispatcher_) { + Event e(action + ":released", "InputManager"); + dispatcher_->dispatchEvent(e); + } + return true; } - return true; - } - case SDL_EVENT_MOUSE_MOTION: { - mouseX_ = event.motion.x; - mouseY_ = event.motion.y; - mouseDeltaX_ += event.motion.xrel; - mouseDeltaY_ += event.motion.yrel; - return true; - } + case SDL_EVENT_MOUSE_MOTION: { + mouseX_ = event.motion.x; + mouseY_ = event.motion.y; + mouseDeltaX_ += event.motion.xrel; + mouseDeltaY_ += event.motion.yrel; + return true; + } - case SDL_EVENT_MOUSE_BUTTON_DOWN: { - int idx = event.button.button - 1; - if (idx >= 0 && idx < 5) { - mouseButtons_[static_cast(idx)] = true; + case SDL_EVENT_MOUSE_BUTTON_DOWN: { + int idx = event.button.button - 1; + if (idx >= 0 && idx < 5) { + mouseButtons_[static_cast(idx)] = true; + } + return true; } - return true; - } - case SDL_EVENT_MOUSE_BUTTON_UP: { - int idx = event.button.button - 1; - if (idx >= 0 && idx < 5) { - mouseButtons_[static_cast(idx)] = false; + case SDL_EVENT_MOUSE_BUTTON_UP: { + int idx = event.button.button - 1; + if (idx >= 0 && idx < 5) { + mouseButtons_[static_cast(idx)] = false; + } + return true; } - return true; - } - default: - return false; + default: + return false; } } -float InputManager::mouseX() const { return mouseX_; } -float InputManager::mouseY() const { return mouseY_; } -float InputManager::mouseDeltaX() const { return mouseDeltaX_; } -float InputManager::mouseDeltaY() const { return mouseDeltaY_; } +float InputManager::mouseX() const { + return mouseX_; +} +float InputManager::mouseY() const { + return mouseY_; +} +float InputManager::mouseDeltaX() const { + return mouseDeltaX_; +} +float InputManager::mouseDeltaY() const { + return mouseDeltaY_; +} bool InputManager::mouseButton(int button) const { int idx = button - 1; diff --git a/src/core/Lifecycle.cc b/src/core/Lifecycle.cc index 2dbb84c9..7a084bc8 100644 --- a/src/core/Lifecycle.cc +++ b/src/core/Lifecycle.cc @@ -6,15 +6,22 @@ namespace fabric { std::string lifecycleStateToString(LifecycleState state) { - switch (state) { - case LifecycleState::Created: return "Created"; - case LifecycleState::Initialized: return "Initialized"; - case LifecycleState::Rendered: return "Rendered"; - case LifecycleState::Updating: return "Updating"; - case LifecycleState::Suspended: return "Suspended"; - case LifecycleState::Destroyed: return "Destroyed"; - default: return "Unknown"; - } + switch (state) { + case LifecycleState::Created: + return "Created"; + case LifecycleState::Initialized: + return "Initialized"; + case LifecycleState::Rendered: + return "Rendered"; + case LifecycleState::Updating: + return "Updating"; + case LifecycleState::Suspended: + return "Suspended"; + case LifecycleState::Destroyed: + return "Destroyed"; + default: + return "Unknown"; + } } namespace { @@ -22,60 +29,60 @@ namespace { // Single source of truth for lifecycle transitions, used by both the // constructor (to configure the StateMachine) and the static isValidTransition. const auto& lifecycleTransitions() { - static const std::set> t = { - {LifecycleState::Created, LifecycleState::Initialized}, - {LifecycleState::Created, LifecycleState::Destroyed}, - {LifecycleState::Initialized, LifecycleState::Rendered}, - {LifecycleState::Initialized, LifecycleState::Suspended}, - {LifecycleState::Initialized, LifecycleState::Destroyed}, - {LifecycleState::Rendered, LifecycleState::Updating}, - {LifecycleState::Rendered, LifecycleState::Suspended}, - {LifecycleState::Rendered, LifecycleState::Destroyed}, - {LifecycleState::Updating, LifecycleState::Rendered}, - {LifecycleState::Updating, LifecycleState::Suspended}, - {LifecycleState::Updating, LifecycleState::Destroyed}, - {LifecycleState::Suspended, LifecycleState::Initialized}, - {LifecycleState::Suspended, LifecycleState::Rendered}, - {LifecycleState::Suspended, LifecycleState::Destroyed}, - }; - return t; + static const std::set> t = { + {LifecycleState::Created, LifecycleState::Initialized}, + {LifecycleState::Created, LifecycleState::Destroyed}, + {LifecycleState::Initialized, LifecycleState::Rendered}, + {LifecycleState::Initialized, LifecycleState::Suspended}, + {LifecycleState::Initialized, LifecycleState::Destroyed}, + {LifecycleState::Rendered, LifecycleState::Updating}, + {LifecycleState::Rendered, LifecycleState::Suspended}, + {LifecycleState::Rendered, LifecycleState::Destroyed}, + {LifecycleState::Updating, LifecycleState::Rendered}, + {LifecycleState::Updating, LifecycleState::Suspended}, + {LifecycleState::Updating, LifecycleState::Destroyed}, + {LifecycleState::Suspended, LifecycleState::Initialized}, + {LifecycleState::Suspended, LifecycleState::Rendered}, + {LifecycleState::Suspended, LifecycleState::Destroyed}, + }; + return t; } } // anonymous namespace -LifecycleManager::LifecycleManager() - : sm_(LifecycleState::Created, lifecycleStateToString) { - for (const auto& [from, to] : lifecycleTransitions()) { - sm_.addTransition(from, to); - } +LifecycleManager::LifecycleManager() : sm_(LifecycleState::Created, lifecycleStateToString) { + for (const auto& [from, to] : lifecycleTransitions()) { + sm_.addTransition(from, to); + } } void LifecycleManager::setState(LifecycleState state) { - auto previous = sm_.getState(); - sm_.setState(state); - FABRIC_LOG_DEBUG("Lifecycle: transition {} -> {}", lifecycleStateToString(previous), lifecycleStateToString(state)); + auto previous = sm_.getState(); + sm_.setState(state); + FABRIC_LOG_DEBUG("Lifecycle: transition {} -> {}", lifecycleStateToString(previous), lifecycleStateToString(state)); } LifecycleState LifecycleManager::getState() const { - return sm_.getState(); + return sm_.getState(); } std::string LifecycleManager::addHook(LifecycleState state, const LifecycleHook& hook) { - return sm_.addHook(state, hook); + return sm_.addHook(state, hook); } std::string LifecycleManager::addTransitionHook(LifecycleState fromState, LifecycleState toState, const LifecycleHook& hook) { - return sm_.addTransitionHook(fromState, toState, hook); + return sm_.addTransitionHook(fromState, toState, hook); } bool LifecycleManager::removeHook(const std::string& hookId) { - return sm_.removeHook(hookId); + return sm_.removeHook(hookId); } bool LifecycleManager::isValidTransition(LifecycleState fromState, LifecycleState toState) { - if (fromState == toState) return true; - return lifecycleTransitions().count({fromState, toState}) > 0; + if (fromState == toState) + return true; + return lifecycleTransitions().count({fromState, toState}) > 0; } } // namespace fabric diff --git a/src/core/Log.cc b/src/core/Log.cc index 7d4bb26d..4002693f 100644 --- a/src/core/Log.cc +++ b/src/core/Log.cc @@ -9,76 +9,67 @@ namespace fabric::log { namespace { - quill::Logger* g_logger = nullptr; +quill::Logger* g_logger = nullptr; } void init() { - quill::BackendOptions backend_opts; - backend_opts.thread_name = "FabricLog"; - backend_opts.wait_for_queues_to_empty_before_exit = true; + quill::BackendOptions backend_opts; + backend_opts.thread_name = "FabricLog"; + backend_opts.wait_for_queues_to_empty_before_exit = true; - quill::Backend::start(backend_opts); + quill::Backend::start(backend_opts); - auto console_sink = - quill::Frontend::create_or_get_sink("console"); + auto console_sink = quill::Frontend::create_or_get_sink("console"); - quill::PatternFormatterOptions pattern; - pattern.format_pattern = - "%(time) [%(thread_id)] %(short_source_location:<28) " - "%(log_level:<9) %(message)"; - pattern.timestamp_pattern = "%H:%M:%S.%Qms"; + quill::PatternFormatterOptions pattern; + pattern.format_pattern = "%(time) [%(thread_id)] %(short_source_location:<28) " + "%(log_level:<9) %(message)"; + pattern.timestamp_pattern = "%H:%M:%S.%Qms"; - g_logger = quill::Frontend::create_or_get_logger( - "fabric", std::move(console_sink), pattern); - g_logger->set_log_level(quill::LogLevel::Info); + g_logger = quill::Frontend::create_or_get_logger("fabric", std::move(console_sink), pattern); + g_logger->set_log_level(quill::LogLevel::Info); } void init(const char* log_file_path) { - quill::BackendOptions backend_opts; - backend_opts.thread_name = "FabricLog"; - backend_opts.wait_for_queues_to_empty_before_exit = true; - - quill::Backend::start(backend_opts); - - auto console_sink = - quill::Frontend::create_or_get_sink("console"); - - auto file_sink = quill::Frontend::create_or_get_sink( - log_file_path, - []() { - quill::FileSinkConfig cfg; - cfg.set_open_mode('w'); - cfg.set_filename_append_option( - quill::FilenameAppendOption::StartDateTime); - return cfg; + quill::BackendOptions backend_opts; + backend_opts.thread_name = "FabricLog"; + backend_opts.wait_for_queues_to_empty_before_exit = true; + + quill::Backend::start(backend_opts); + + auto console_sink = quill::Frontend::create_or_get_sink("console"); + + auto file_sink = quill::Frontend::create_or_get_sink(log_file_path, []() { + quill::FileSinkConfig cfg; + cfg.set_open_mode('w'); + cfg.set_filename_append_option(quill::FilenameAppendOption::StartDateTime); + return cfg; }()); - quill::PatternFormatterOptions pattern; - pattern.format_pattern = - "%(time) [%(thread_id)] %(short_source_location:<28) " - "%(log_level:<9) %(message)"; - pattern.timestamp_pattern = "%H:%M:%S.%Qms"; + quill::PatternFormatterOptions pattern; + pattern.format_pattern = "%(time) [%(thread_id)] %(short_source_location:<28) " + "%(log_level:<9) %(message)"; + pattern.timestamp_pattern = "%H:%M:%S.%Qms"; - g_logger = quill::Frontend::create_or_get_logger( - "fabric", {console_sink, file_sink}, pattern); - g_logger->set_log_level(quill::LogLevel::Info); + g_logger = quill::Frontend::create_or_get_logger("fabric", {console_sink, file_sink}, pattern); + g_logger->set_log_level(quill::LogLevel::Info); } void shutdown() { - if (g_logger) { - g_logger->flush_log(); - } - quill::Backend::stop(); + if (g_logger) { + g_logger->flush_log(); + } + quill::Backend::stop(); } quill::Logger* logger() { - return g_logger; + return g_logger; } void setLevel(quill::LogLevel level) { - if (g_logger) { - g_logger->set_log_level(level); - } + if (g_logger) { + g_logger->set_log_level(level); + } } } // namespace fabric::log diff --git a/src/core/Plugin.cc b/src/core/Plugin.cc index c78d61d2..fd244197 100644 --- a/src/core/Plugin.cc +++ b/src/core/Plugin.cc @@ -1,198 +1,197 @@ #include "fabric/core/Plugin.hh" -#include "fabric/utils/ErrorHandling.hh" #include "fabric/core/Log.hh" +#include "fabric/utils/ErrorHandling.hh" #include #include namespace fabric { void PluginManager::registerPlugin(const std::string& name, const PluginFactory& factory) { - std::lock_guard lock(pluginMutex); - - if (name.empty()) { - throwError("Plugin name cannot be empty"); - } - - if (!factory) { - throwError("Plugin factory cannot be null"); - } - - if (pluginFactories.find(name) != pluginFactories.end()) { - throwError("Plugin '" + name + "' is already registered"); - } - - pluginFactories[name] = factory; - FABRIC_LOG_DEBUG("Registered plugin '{}'", name); + std::lock_guard lock(pluginMutex); + + if (name.empty()) { + throwError("Plugin name cannot be empty"); + } + + if (!factory) { + throwError("Plugin factory cannot be null"); + } + + if (pluginFactories.find(name) != pluginFactories.end()) { + throwError("Plugin '" + name + "' is already registered"); + } + + pluginFactories[name] = factory; + FABRIC_LOG_DEBUG("Registered plugin '{}'", name); } bool PluginManager::loadPlugin(const std::string& name) { - std::lock_guard lock(pluginMutex); - - // Check if already loaded - if (loadedPlugins.find(name) != loadedPlugins.end()) { - FABRIC_LOG_WARN("Plugin '{}' is already loaded", name); - return true; - } - - // Find factory - auto factoryIt = pluginFactories.find(name); - if (factoryIt == pluginFactories.end()) { - FABRIC_LOG_ERROR("Plugin '{}' is not registered", name); - return false; - } - - try { - // Create plugin instance - auto plugin = factoryIt->second(); - if (!plugin) { - FABRIC_LOG_ERROR("Failed to create plugin '{}'", name); - return false; + std::lock_guard lock(pluginMutex); + + // Check if already loaded + if (loadedPlugins.find(name) != loadedPlugins.end()) { + FABRIC_LOG_WARN("Plugin '{}' is already loaded", name); + return true; + } + + // Find factory + auto factoryIt = pluginFactories.find(name); + if (factoryIt == pluginFactories.end()) { + FABRIC_LOG_ERROR("Plugin '{}' is not registered", name); + return false; + } + + try { + // Create plugin instance + auto plugin = factoryIt->second(); + if (!plugin) { + FABRIC_LOG_ERROR("Failed to create plugin '{}'", name); + return false; + } + + // Add to loaded plugins + loadedPlugins[name] = plugin; + FABRIC_LOG_INFO("Loaded plugin '{}' ({}) by {}", name, plugin->getVersion(), plugin->getAuthor()); + + return true; + } catch (const std::exception& e) { + FABRIC_LOG_ERROR("Exception loading plugin '{}': {}", name, e.what()); + return false; + } catch (...) { + FABRIC_LOG_ERROR("Unknown exception loading plugin '{}'", name); + return false; } - - // Add to loaded plugins - loadedPlugins[name] = plugin; - FABRIC_LOG_INFO("Loaded plugin '{}' ({}) by {}", name, - plugin->getVersion(), plugin->getAuthor()); - - return true; - } catch (const std::exception& e) { - FABRIC_LOG_ERROR("Exception loading plugin '{}': {}", name, e.what()); - return false; - } catch (...) { - FABRIC_LOG_ERROR("Unknown exception loading plugin '{}'", name); - return false; - } } bool PluginManager::unloadPlugin(const std::string& name) { - std::shared_ptr pluginToUnload; - - { - std::lock_guard lock(pluginMutex); - - auto it = loadedPlugins.find(name); - if (it == loadedPlugins.end()) { - FABRIC_LOG_WARN("Plugin '{}' is not loaded", name); - return false; + std::shared_ptr pluginToUnload; + + { + std::lock_guard lock(pluginMutex); + + auto it = loadedPlugins.find(name); + if (it == loadedPlugins.end()) { + FABRIC_LOG_WARN("Plugin '{}' is not loaded", name); + return false; + } + + // Store the plugin to unload outside the lock + pluginToUnload = it->second; + + // Remove from loaded plugins immediately to prevent cyclic dependencies + loadedPlugins.erase(it); } - - // Store the plugin to unload outside the lock - pluginToUnload = it->second; - - // Remove from loaded plugins immediately to prevent cyclic dependencies - loadedPlugins.erase(it); - } - - try { - // Shut down the plugin outside the lock to prevent deadlocks - if (pluginToUnload) { - pluginToUnload->shutdown(); + + try { + // Shut down the plugin outside the lock to prevent deadlocks + if (pluginToUnload) { + pluginToUnload->shutdown(); + } + + FABRIC_LOG_INFO("Unloaded plugin '{}'", name); + return true; + } catch (const std::exception& e) { + FABRIC_LOG_ERROR("Exception unloading plugin '{}': {}", name, e.what()); + return false; + } catch (...) { + FABRIC_LOG_ERROR("Unknown exception unloading plugin '{}'", name); + return false; } - - FABRIC_LOG_INFO("Unloaded plugin '{}'", name); - return true; - } catch (const std::exception& e) { - FABRIC_LOG_ERROR("Exception unloading plugin '{}': {}", name, e.what()); - return false; - } catch (...) { - FABRIC_LOG_ERROR("Unknown exception unloading plugin '{}'", name); - return false; - } } std::shared_ptr PluginManager::getPlugin(const std::string& name) const { - std::lock_guard lock(pluginMutex); - - auto it = loadedPlugins.find(name); - if (it == loadedPlugins.end()) { - return nullptr; - } - - return it->second; + std::lock_guard lock(pluginMutex); + + auto it = loadedPlugins.find(name); + if (it == loadedPlugins.end()) { + return nullptr; + } + + return it->second; } std::unordered_map> PluginManager::getPlugins() const { - std::lock_guard lock(pluginMutex); - return loadedPlugins; // Return a copy for thread safety + std::lock_guard lock(pluginMutex); + return loadedPlugins; // Return a copy for thread safety } bool PluginManager::initializeAll() { - // Create a copy of the plugins to avoid holding the lock during initialization - std::vector>> plugins; - - { - std::lock_guard lock(pluginMutex); - plugins.reserve(loadedPlugins.size()); - for (const auto& pair : loadedPlugins) { - plugins.push_back(pair); - } - } - - bool success = true; - - for (const auto& [name, plugin] : plugins) { - if (!plugin) { - FABRIC_LOG_ERROR("Null plugin reference for '{}'", name); - success = false; - continue; + // Create a copy of the plugins to avoid holding the lock during initialization + std::vector>> plugins; + + { + std::lock_guard lock(pluginMutex); + plugins.reserve(loadedPlugins.size()); + for (const auto& pair : loadedPlugins) { + plugins.push_back(pair); + } } - - try { - if (!plugin->initialize()) { - FABRIC_LOG_ERROR("Failed to initialize plugin '{}'", name); - success = false; - } else { - FABRIC_LOG_INFO("Initialized plugin '{}'", name); - } - } catch (const std::exception& e) { - FABRIC_LOG_ERROR("Exception initializing plugin '{}': {}", name, e.what()); - success = false; - } catch (...) { - FABRIC_LOG_ERROR("Unknown exception initializing plugin '{}'", name); - success = false; + + bool success = true; + + for (const auto& [name, plugin] : plugins) { + if (!plugin) { + FABRIC_LOG_ERROR("Null plugin reference for '{}'", name); + success = false; + continue; + } + + try { + if (!plugin->initialize()) { + FABRIC_LOG_ERROR("Failed to initialize plugin '{}'", name); + success = false; + } else { + FABRIC_LOG_INFO("Initialized plugin '{}'", name); + } + } catch (const std::exception& e) { + FABRIC_LOG_ERROR("Exception initializing plugin '{}': {}", name, e.what()); + success = false; + } catch (...) { + FABRIC_LOG_ERROR("Unknown exception initializing plugin '{}'", name); + success = false; + } } - } - - return success; + + return success; } void PluginManager::shutdownAll() { - // Copy all plugins to a vector for shutdown - // This allows us to control shutdown order and handle dependencies - std::vector>> plugins; - - { - std::lock_guard lock(pluginMutex); - plugins.reserve(loadedPlugins.size()); - for (const auto& pair : loadedPlugins) { - plugins.push_back(pair); - } - - // Clear the loaded plugins container first to prevent cyclical shutdown dependencies - loadedPlugins.clear(); - } - - // NOTE: This reversal does not guarantee dependency-correct shutdown order. - // Plugins are stored in an unordered_map, so iteration order (and therefore - // reversal order) is implementation-defined and unrelated to load sequence. - // Proper dependency-ordered shutdown requires an explicit dependency graph. - std::reverse(plugins.begin(), plugins.end()); - - for (const auto& [name, plugin] : plugins) { - if (!plugin) { - FABRIC_LOG_WARN("Null plugin reference for '{}' during shutdown", name); - continue; + // Copy all plugins to a vector for shutdown + // This allows us to control shutdown order and handle dependencies + std::vector>> plugins; + + { + std::lock_guard lock(pluginMutex); + plugins.reserve(loadedPlugins.size()); + for (const auto& pair : loadedPlugins) { + plugins.push_back(pair); + } + + // Clear the loaded plugins container first to prevent cyclical shutdown dependencies + loadedPlugins.clear(); } - - try { - plugin->shutdown(); - FABRIC_LOG_INFO("Shut down plugin '{}'", name); - } catch (const std::exception& e) { - FABRIC_LOG_ERROR("Exception shutting down plugin '{}': {}", name, e.what()); - } catch (...) { - FABRIC_LOG_ERROR("Unknown exception shutting down plugin '{}'", name); + + // NOTE: This reversal does not guarantee dependency-correct shutdown order. + // Plugins are stored in an unordered_map, so iteration order (and therefore + // reversal order) is implementation-defined and unrelated to load sequence. + // Proper dependency-ordered shutdown requires an explicit dependency graph. + std::reverse(plugins.begin(), plugins.end()); + + for (const auto& [name, plugin] : plugins) { + if (!plugin) { + FABRIC_LOG_WARN("Null plugin reference for '{}' during shutdown", name); + continue; + } + + try { + plugin->shutdown(); + FABRIC_LOG_INFO("Shut down plugin '{}'", name); + } catch (const std::exception& e) { + FABRIC_LOG_ERROR("Exception shutting down plugin '{}': {}", name, e.what()); + } catch (...) { + FABRIC_LOG_ERROR("Unknown exception shutting down plugin '{}'", name); + } } - } } -} // namespace fabric \ No newline at end of file +} // namespace fabric diff --git a/src/core/Rendering.cc b/src/core/Rendering.cc index 091f521d..679de434 100644 --- a/src/core/Rendering.cc +++ b/src/core/Rendering.cc @@ -7,52 +7,31 @@ namespace fabric { // AABB -AABB::AABB() - : min(Vec3f(0.0f, 0.0f, 0.0f)), - max(Vec3f(0.0f, 0.0f, 0.0f)) {} +AABB::AABB() : min(Vec3f(0.0f, 0.0f, 0.0f)), max(Vec3f(0.0f, 0.0f, 0.0f)) {} -AABB::AABB(const Vec3f& min, const Vec3f& max) - : min(min), max(max) {} +AABB::AABB(const Vec3f& min, const Vec3f& max) : min(min), max(max) {} Vec3f AABB::center() const { - return Vec3f( - (min.x + max.x) * 0.5f, - (min.y + max.y) * 0.5f, - (min.z + max.z) * 0.5f - ); + return Vec3f((min.x + max.x) * 0.5f, (min.y + max.y) * 0.5f, (min.z + max.z) * 0.5f); } Vec3f AABB::extents() const { - return Vec3f( - (max.x - min.x) * 0.5f, - (max.y - min.y) * 0.5f, - (max.z - min.z) * 0.5f - ); + return Vec3f((max.x - min.x) * 0.5f, (max.y - min.y) * 0.5f, (max.z - min.z) * 0.5f); } void AABB::expand(const Vec3f& point) { - min = Vec3f( - std::min(min.x, point.x), - std::min(min.y, point.y), - std::min(min.z, point.z) - ); - max = Vec3f( - std::max(max.x, point.x), - std::max(max.y, point.y), - std::max(max.z, point.z) - ); + min = Vec3f(std::min(min.x, point.x), std::min(min.y, point.y), std::min(min.z, point.z)); + max = Vec3f(std::max(max.x, point.x), std::max(max.y, point.y), std::max(max.z, point.z)); } bool AABB::contains(const Vec3f& point) const { - return point.x >= min.x && point.x <= max.x - && point.y >= min.y && point.y <= max.y - && point.z >= min.z && point.z <= max.z; + return point.x >= min.x && point.x <= max.x && point.y >= min.y && point.y <= max.y && point.z >= min.z && + point.z <= max.z; } bool AABB::intersects(const AABB& other) const { - return min.x <= other.max.x && max.x >= other.min.x - && min.y <= other.max.y && max.y >= other.min.y - && min.z <= other.max.z && max.z >= other.min.z; + return min.x <= other.max.x && max.x >= other.min.x && min.y <= other.max.y && max.y >= other.min.y && + min.z <= other.max.z && max.z >= other.min.z; } // Plane @@ -77,20 +56,22 @@ void Plane::normalize() { void Frustum::extractFromVP(const float* vp) { // Column-major access: element at row r, col c = vp[c * 4 + r] // Row vectors of the VP matrix (transposed access) - auto row = [&](int r, int c) { return vp[c * 4 + r]; }; + auto row = [&](int r, int c) { + return vp[c * 4 + r]; + }; // Left: row3 + row0 - planes[0] = {row(3,0) + row(0,0), row(3,1) + row(0,1), row(3,2) + row(0,2), row(3,3) + row(0,3)}; + planes[0] = {row(3, 0) + row(0, 0), row(3, 1) + row(0, 1), row(3, 2) + row(0, 2), row(3, 3) + row(0, 3)}; // Right: row3 - row0 - planes[1] = {row(3,0) - row(0,0), row(3,1) - row(0,1), row(3,2) - row(0,2), row(3,3) - row(0,3)}; + planes[1] = {row(3, 0) - row(0, 0), row(3, 1) - row(0, 1), row(3, 2) - row(0, 2), row(3, 3) - row(0, 3)}; // Bottom: row3 + row1 - planes[2] = {row(3,0) + row(1,0), row(3,1) + row(1,1), row(3,2) + row(1,2), row(3,3) + row(1,3)}; + planes[2] = {row(3, 0) + row(1, 0), row(3, 1) + row(1, 1), row(3, 2) + row(1, 2), row(3, 3) + row(1, 3)}; // Top: row3 - row1 - planes[3] = {row(3,0) - row(1,0), row(3,1) - row(1,1), row(3,2) - row(1,2), row(3,3) - row(1,3)}; + planes[3] = {row(3, 0) - row(1, 0), row(3, 1) - row(1, 1), row(3, 2) - row(1, 2), row(3, 3) - row(1, 3)}; // Near: row3 + row2 - planes[4] = {row(3,0) + row(2,0), row(3,1) + row(2,1), row(3,2) + row(2,2), row(3,3) + row(2,3)}; + planes[4] = {row(3, 0) + row(2, 0), row(3, 1) + row(2, 1), row(3, 2) + row(2, 2), row(3, 3) + row(2, 3)}; // Far: row3 - row2 - planes[5] = {row(3,0) - row(2,0), row(3,1) - row(2,1), row(3,2) - row(2,2), row(3,3) - row(2,3)}; + planes[5] = {row(3, 0) - row(2, 0), row(3, 1) - row(2, 1), row(3, 2) - row(2, 2), row(3, 3) - row(2, 3)}; for (auto& plane : planes) { plane.normalize(); @@ -102,16 +83,10 @@ CullResult Frustum::testAABB(const AABB& aabb) const { for (const auto& plane : planes) { // Find the positive and negative vertices relative to the plane normal - Vec3f pVertex( - plane.a >= 0.0f ? aabb.max.x : aabb.min.x, - plane.b >= 0.0f ? aabb.max.y : aabb.min.y, - plane.c >= 0.0f ? aabb.max.z : aabb.min.z - ); - Vec3f nVertex( - plane.a >= 0.0f ? aabb.min.x : aabb.max.x, - plane.b >= 0.0f ? aabb.min.y : aabb.max.y, - plane.c >= 0.0f ? aabb.min.z : aabb.max.z - ); + Vec3f pVertex(plane.a >= 0.0f ? aabb.max.x : aabb.min.x, plane.b >= 0.0f ? aabb.max.y : aabb.min.y, + plane.c >= 0.0f ? aabb.max.z : aabb.min.z); + Vec3f nVertex(plane.a >= 0.0f ? aabb.min.x : aabb.max.x, plane.b >= 0.0f ? aabb.min.y : aabb.max.y, + plane.c >= 0.0f ? aabb.min.z : aabb.max.z); if (plane.distanceToPoint(pVertex) < 0.0f) { return CullResult::Outside; @@ -133,9 +108,7 @@ void RenderList::addDrawCall(const DrawCall& call) { void RenderList::sortByKey() { FABRIC_ZONE_SCOPED_N("RenderList::sortByKey"); std::sort(drawCalls_.begin(), drawCalls_.end(), - [](const DrawCall& a, const DrawCall& b) { - return a.sortKey < b.sortKey; - }); + [](const DrawCall& a, const DrawCall& b) { return a.sortKey < b.sortKey; }); } void RenderList::clear() { @@ -156,31 +129,22 @@ bool RenderList::empty() const { // TransformInterpolator -Transform TransformInterpolator::interpolate( - const Transform& prev, - const Transform& current, - float alpha -) { +Transform TransformInterpolator::interpolate(const Transform& prev, const Transform& current, + float alpha) { Transform result; - result.setPosition(Vector3::lerp( - prev.getPosition(), current.getPosition(), alpha)); + result.setPosition(Vector3::lerp(prev.getPosition(), current.getPosition(), alpha)); - result.setRotation(Quaternion::slerp( - prev.getRotation(), current.getRotation(), alpha)); + result.setRotation(Quaternion::slerp(prev.getRotation(), current.getRotation(), alpha)); - result.setScale(Vector3::lerp( - prev.getScale(), current.getScale(), alpha)); + result.setScale(Vector3::lerp(prev.getScale(), current.getScale(), alpha)); return result; } // FrustumCuller -std::vector FrustumCuller::cull( - const float* viewProjection, - flecs::world& world -) { +std::vector FrustumCuller::cull(const float* viewProjection, flecs::world& world) { FABRIC_ZONE_SCOPED_N("FrustumCuller::cull"); Frustum frustum; @@ -190,17 +154,15 @@ std::vector FrustumCuller::cull( // Flat iteration: test each SceneEntity independently world.each([&](flecs::entity e, const Position&) { - if (!e.has()) return; + if (!e.has()) + return; - const auto* bb = e.get(); + const auto* bb = e.try_get(); if (bb) { - AABB localAABB( - Vec3f(bb->minX, bb->minY, bb->minZ), - Vec3f(bb->maxX, bb->maxY, bb->maxZ) - ); + AABB localAABB(Vec3f(bb->minX, bb->minY, bb->minZ), Vec3f(bb->maxX, bb->maxY, bb->maxZ)); // Transform AABB to world space using LocalToWorld if available - const auto* ltw = e.get(); + const auto* ltw = e.try_get(); if (ltw) { Matrix4x4 m(ltw->matrix); // Transform all 8 corners and re-fit the AABB @@ -209,11 +171,9 @@ std::vector FrustumCuller::cull( for (int cx = 0; cx < 2; ++cx) { for (int cy = 0; cy < 2; ++cy) { for (int cz = 0; cz < 2; ++cz) { - Vec3f corner( - cx == 0 ? localAABB.min.x : localAABB.max.x, - cy == 0 ? localAABB.min.y : localAABB.max.y, - cz == 0 ? localAABB.min.z : localAABB.max.z - ); + Vec3f corner(cx == 0 ? localAABB.min.x : localAABB.max.x, + cy == 0 ? localAABB.min.y : localAABB.max.y, + cz == 0 ? localAABB.min.z : localAABB.max.z); auto worldCorner = m.transformPoint(corner); if (first) { worldAABB = AABB(worldCorner, worldCorner); @@ -224,9 +184,11 @@ std::vector FrustumCuller::cull( } } } - if (frustum.testAABB(worldAABB) == CullResult::Outside) return; + if (frustum.testAABB(worldAABB) == CullResult::Outside) + return; } else { - if (frustum.testAABB(localAABB) == CullResult::Outside) return; + if (frustum.testAABB(localAABB) == CullResult::Outside) + return; } } diff --git a/src/core/Resource.cc b/src/core/Resource.cc index f5074690..2bb37a86 100644 --- a/src/core/Resource.cc +++ b/src/core/Resource.cc @@ -5,7 +5,8 @@ namespace fabric { // Initialize static members std::mutex ResourceFactory::mutex_; -std::unordered_map(const std::string&)>> ResourceFactory::factories_; +std::unordered_map(const std::string&)>> + ResourceFactory::factories_; bool ResourceFactory::isTypeRegistered(const std::string& typeId) { std::lock_guard lock(mutex_); @@ -23,4 +24,4 @@ std::shared_ptr ResourceFactory::create(const std::string& typeId, con return it->second(id); } -} // namespace fabric \ No newline at end of file +} // namespace fabric diff --git a/src/core/ResourceHub.cc b/src/core/ResourceHub.cc index b407b2cc..3c67df9d 100644 --- a/src/core/ResourceHub.cc +++ b/src/core/ResourceHub.cc @@ -1,9 +1,9 @@ #include "fabric/core/ResourceHub.hh" -#include "fabric/utils/ErrorHandling.hh" #include "fabric/core/Log.hh" +#include "fabric/utils/ErrorHandling.hh" #include -#include #include +#include #ifdef __APPLE__ #include @@ -13,20 +13,22 @@ namespace fabric { // Worker thread function void ResourceHub::workerThreadFunc() { - // Call the process queue function - this->processLoadQueue(); + // Call the process queue function + this->processLoadQueue(); } // Enforce budget wrapper -void ResourceHub::enforceBudget() { enforceMemoryBudget(); } +void ResourceHub::enforceBudget() { + enforceMemoryBudget(); +} // Destructor implementation -ResourceHub::~ResourceHub() { +ResourceHub::~ResourceHub() { try { // Use timeout protection for shutdown operations auto shutdownTimeoutMs = 1000; // 1 second timeout auto startTime = std::chrono::steady_clock::now(); - + // Create a separate thread for shutdown to prevent blocking std::atomic shutdownCompleted{false}; std::thread shutdownThread([this, &shutdownCompleted]() { @@ -41,7 +43,7 @@ ResourceHub::~ResourceHub() { shutdownCompleted = true; } }); - + // Wait for shutdown to complete with timeout while (!shutdownCompleted) { auto now = std::chrono::steady_clock::now(); @@ -51,7 +53,7 @@ ResourceHub::~ResourceHub() { } std::this_thread::sleep_for(std::chrono::milliseconds(10)); } - + // Detach the thread if it's still running if (shutdownThread.joinable()) { shutdownThread.detach(); @@ -66,1060 +68,1035 @@ ResourceHub::~ResourceHub() { // Shutdown implementation void ResourceHub::shutdown() { - // Use timeout constants - constexpr int MUTEX_TIMEOUT_MS = 100; - constexpr int THREAD_JOIN_TIMEOUT_MS = 500; - constexpr int OVERALL_TIMEOUT_MS = 2000; - - auto startTime = std::chrono::steady_clock::now(); - - // Timeout checker function - auto isTimedOut = [&startTime, OVERALL_TIMEOUT_MS]() -> bool { - return std::chrono::duration_cast( - std::chrono::steady_clock::now() - startTime) - .count() > OVERALL_TIMEOUT_MS; - }; - - try { - // Signal worker threads to stop with timeout protection - bool lockAcquired = false; - - // Try to acquire queue mutex with timeout - auto queueLockStart = std::chrono::steady_clock::now(); - while (!queueMutex_.try_lock()) { - if (std::chrono::duration_cast( - std::chrono::steady_clock::now() - queueLockStart).count() > MUTEX_TIMEOUT_MS) { - FABRIC_LOG_WARN("Failed to acquire queue mutex in shutdown"); - break; - } - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - - // Set shutdown flag (thread-safe since it's atomic) - shutdown_ = true; - - // If we acquired the lock, we can clear the queue and unlock - if (queueMutex_.try_lock()) { - lockAcquired = true; - - // Clear the queue - while (!loadQueue_.empty()) { - loadQueue_.pop(); - } - - // Release the lock - queueMutex_.unlock(); - } - - // Notify all threads (this is thread-safe) - queueCondition_.notify_all(); - - // Create a copy of worker threads to prevent race conditions - std::vector> threadsCopy; - - // Safely get threads - with timeout - bool threadCopySucceeded = false; + // Use timeout constants + constexpr int MUTEX_TIMEOUT_MS = 100; + constexpr int THREAD_JOIN_TIMEOUT_MS = 500; + constexpr int OVERALL_TIMEOUT_MS = 2000; + + auto startTime = std::chrono::steady_clock::now(); + + // Timeout checker function + auto isTimedOut = [&startTime, OVERALL_TIMEOUT_MS]() -> bool { + return std::chrono::duration_cast(std::chrono::steady_clock::now() - startTime) + .count() > OVERALL_TIMEOUT_MS; + }; + try { - if (threadControlMutex_.try_lock_for(std::chrono::milliseconds(MUTEX_TIMEOUT_MS))) { - threadsCopy = std::move(workerThreads_); - workerThreads_.clear(); - threadControlMutex_.unlock(); - threadCopySucceeded = true; - } - } catch (...) { - FABRIC_LOG_WARN("ResourceHub: failed to copy worker threads during shutdown"); - if (threadControlMutex_.try_lock()) { - threadControlMutex_.unlock(); - } - } - - // If we couldn't copy threads, try to use the original vector with care - std::vector threadsToJoin; - if (threadCopySucceeded) { - // Wait for threads to finish with timeout - for (auto &thread : threadsCopy) { - if (thread && thread->joinable()) { - threadsToJoin.push_back(thread.get()); + // Signal worker threads to stop with timeout protection + bool lockAcquired = false; + + // Try to acquire queue mutex with timeout + auto queueLockStart = std::chrono::steady_clock::now(); + while (!queueMutex_.try_lock()) { + if (std::chrono::duration_cast(std::chrono::steady_clock::now() - queueLockStart) + .count() > MUTEX_TIMEOUT_MS) { + FABRIC_LOG_WARN("Failed to acquire queue mutex in shutdown"); + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); } - } - } else { - // Try to read the original vector (less safe) - for (auto &thread : workerThreads_) { - if (thread && thread->joinable()) { - threadsToJoin.push_back(thread.get()); + + // Set shutdown flag (thread-safe since it's atomic) + shutdown_ = true; + + // If we acquired the lock, we can clear the queue and unlock + if (queueMutex_.try_lock()) { + lockAcquired = true; + + // Clear the queue + while (!loadQueue_.empty()) { + loadQueue_.pop(); + } + + // Release the lock + queueMutex_.unlock(); } - } - } - - // Join threads with timeout protection - for (auto thread : threadsToJoin) { - if (isTimedOut()) { - FABRIC_LOG_WARN("Shutdown timed out during thread joining"); - break; - } - - // Use a timeout thread to join with timeout - std::atomic joinCompleted{false}; - std::thread joiner([thread, &joinCompleted]() { - if (thread->joinable()) { - thread->join(); + + // Notify all threads (this is thread-safe) + queueCondition_.notify_all(); + + // Create a copy of worker threads to prevent race conditions + std::vector> threadsCopy; + + // Safely get threads - with timeout + bool threadCopySucceeded = false; + try { + if (threadControlMutex_.try_lock_for(std::chrono::milliseconds(MUTEX_TIMEOUT_MS))) { + threadsCopy = std::move(workerThreads_); + workerThreads_.clear(); + threadControlMutex_.unlock(); + threadCopySucceeded = true; + } + } catch (...) { + FABRIC_LOG_WARN("ResourceHub: failed to copy worker threads during shutdown"); + if (threadControlMutex_.try_lock()) { + threadControlMutex_.unlock(); + } } - joinCompleted = true; - }); - - // Wait for join to complete with timeout - auto joinStart = std::chrono::steady_clock::now(); - while (!joinCompleted) { - if (std::chrono::duration_cast( - std::chrono::steady_clock::now() - joinStart).count() > THREAD_JOIN_TIMEOUT_MS) { - FABRIC_LOG_WARN("Thread join timed out in shutdown"); - break; + + // If we couldn't copy threads, try to use the original vector with care + std::vector threadsToJoin; + if (threadCopySucceeded) { + // Wait for threads to finish with timeout + for (auto& thread : threadsCopy) { + if (thread && thread->joinable()) { + threadsToJoin.push_back(thread.get()); + } + } + } else { + // Try to read the original vector (less safe) + for (auto& thread : workerThreads_) { + if (thread && thread->joinable()) { + threadsToJoin.push_back(thread.get()); + } + } } - std::this_thread::sleep_for(std::chrono::milliseconds(5)); - } - - // Detach the joiner thread if it's still running - if (joiner.joinable()) { - joiner.detach(); - } - } - - // Clear threads - if (threadCopySucceeded) { - threadsCopy.clear(); - } else { - // Try to clear the original vector, with timeout protection - if (threadControlMutex_.try_lock_for(std::chrono::milliseconds(MUTEX_TIMEOUT_MS))) { - workerThreads_.clear(); - threadControlMutex_.unlock(); - } - } - - // Unload all resources with timeout protection - if (!isTimedOut()) { - try { - // Attempt to clear resources with a separate timeout - constexpr int CLEAR_TIMEOUT_MS = 500; - auto clearStart = std::chrono::steady_clock::now(); - - std::atomic clearCompleted{false}; - std::thread clearThread([this, &clearCompleted]() { - try { - clear(); - clearCompleted = true; - } catch (...) { - FABRIC_LOG_ERROR("ResourceHub: unknown exception during clear() in shutdown"); - clearCompleted = true; - } - }); - - // Wait for clear to complete with timeout - while (!clearCompleted) { - if (std::chrono::duration_cast( - std::chrono::steady_clock::now() - clearStart).count() > CLEAR_TIMEOUT_MS) { - FABRIC_LOG_WARN("Resource clearing timed out in shutdown"); - break; - } - std::this_thread::sleep_for(std::chrono::milliseconds(5)); + + // Join threads with timeout protection + for (auto thread : threadsToJoin) { + if (isTimedOut()) { + FABRIC_LOG_WARN("Shutdown timed out during thread joining"); + break; + } + + // Use a timeout thread to join with timeout + std::atomic joinCompleted{false}; + std::thread joiner([thread, &joinCompleted]() { + if (thread->joinable()) { + thread->join(); + } + joinCompleted = true; + }); + + // Wait for join to complete with timeout + auto joinStart = std::chrono::steady_clock::now(); + while (!joinCompleted) { + if (std::chrono::duration_cast(std::chrono::steady_clock::now() - joinStart) + .count() > THREAD_JOIN_TIMEOUT_MS) { + FABRIC_LOG_WARN("Thread join timed out in shutdown"); + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + } + + // Detach the joiner thread if it's still running + if (joiner.joinable()) { + joiner.detach(); + } } - - // Detach the clear thread if it's still running - if (clearThread.joinable()) { - clearThread.detach(); + + // Clear threads + if (threadCopySucceeded) { + threadsCopy.clear(); + } else { + // Try to clear the original vector, with timeout protection + if (threadControlMutex_.try_lock_for(std::chrono::milliseconds(MUTEX_TIMEOUT_MS))) { + workerThreads_.clear(); + threadControlMutex_.unlock(); + } } - } catch (const std::exception& e) { - FABRIC_LOG_ERROR("Exception during resource clearing in shutdown: {}", e.what()); - } catch (...) { - FABRIC_LOG_ERROR("Unknown exception during resource clearing in shutdown"); - } + + // Unload all resources with timeout protection + if (!isTimedOut()) { + try { + // Attempt to clear resources with a separate timeout + constexpr int CLEAR_TIMEOUT_MS = 500; + auto clearStart = std::chrono::steady_clock::now(); + + std::atomic clearCompleted{false}; + std::thread clearThread([this, &clearCompleted]() { + try { + clear(); + clearCompleted = true; + } catch (...) { + FABRIC_LOG_ERROR("ResourceHub: unknown exception during clear() in shutdown"); + clearCompleted = true; + } + }); + + // Wait for clear to complete with timeout + while (!clearCompleted) { + if (std::chrono::duration_cast(std::chrono::steady_clock::now() - + clearStart) + .count() > CLEAR_TIMEOUT_MS) { + FABRIC_LOG_WARN("Resource clearing timed out in shutdown"); + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + } + + // Detach the clear thread if it's still running + if (clearThread.joinable()) { + clearThread.detach(); + } + } catch (const std::exception& e) { + FABRIC_LOG_ERROR("Exception during resource clearing in shutdown: {}", e.what()); + } catch (...) { + FABRIC_LOG_ERROR("Unknown exception during resource clearing in shutdown"); + } + } + } catch (const std::exception& e) { + FABRIC_LOG_ERROR("Exception in ResourceHub::shutdown(): {}", e.what()); + } catch (...) { + FABRIC_LOG_ERROR("Unknown exception in ResourceHub::shutdown()"); } - } catch (const std::exception& e) { - FABRIC_LOG_ERROR("Exception in ResourceHub::shutdown(): {}", e.what()); - } catch (...) { - FABRIC_LOG_ERROR("Unknown exception in ResourceHub::shutdown()"); - } } // Implementation of other non-template methods -bool ResourceHub::addDependency(const std::string &dependentId, - const std::string &dependencyId) { - try { - return resourceGraph_.addEdge(dependentId, dependencyId); - } catch (const CycleDetectedException &e) { - FABRIC_LOG_WARN("ResourceHub: cycle detected adding dependency {} -> {}: {}", - dependentId, dependencyId, e.what()); - return false; - } +bool ResourceHub::addDependency(const std::string& dependentId, const std::string& dependencyId) { + try { + return resourceGraph_.addEdge(dependentId, dependencyId); + } catch (const CycleDetectedException& e) { + FABRIC_LOG_WARN("ResourceHub: cycle detected adding dependency {} -> {}: {}", dependentId, dependencyId, + e.what()); + return false; + } } -bool ResourceHub::removeDependency(const std::string &dependentId, - const std::string &dependencyId) { - return resourceGraph_.removeEdge(dependentId, dependencyId); +bool ResourceHub::removeDependency(const std::string& dependentId, const std::string& dependencyId) { + return resourceGraph_.removeEdge(dependentId, dependencyId); } -bool ResourceHub::unload(const std::string &resourceId, bool cascade) { - if (cascade) { - // Unload in dependency order - return unloadRecursive(resourceId); - } else { - auto resourceNode = resourceGraph_.getNode(resourceId); - if (!resourceNode) { - // Resource not found - return false; - } +bool ResourceHub::unload(const std::string& resourceId, bool cascade) { + if (cascade) { + // Unload in dependency order + return unloadRecursive(resourceId); + } else { + auto resourceNode = resourceGraph_.getNode(resourceId); + if (!resourceNode) { + // Resource not found + return false; + } - auto nodeLock = resourceNode->tryLock( - CoordinatedGraph>::LockIntent::NodeModify, - true); - if (!nodeLock || !nodeLock->isLocked()) { - return false; - } + auto nodeLock = + resourceNode->tryLock(CoordinatedGraph>::LockIntent::NodeModify, true); + if (!nodeLock || !nodeLock->isLocked()) { + return false; + } - auto resource = nodeLock->getNode()->getDataNoLock(); + auto resource = nodeLock->getNode()->getDataNoLock(); - // Check if there are dependencies on this resource - auto dependents = resourceGraph_.getInEdges(resourceId); - if (!dependents.empty()) { - // Can't unload if other resources depend on this one - return false; - } + // Check if there are dependencies on this resource + auto dependents = resourceGraph_.getInEdges(resourceId); + if (!dependents.empty()) { + // Can't unload if other resources depend on this one + return false; + } - // Unload the resource - ResourceState state = resource->getState(); - if (state == ResourceState::Loaded) { - resource->unload(); - } + // Unload the resource + ResourceState state = resource->getState(); + if (state == ResourceState::Loaded) { + resource->unload(); + } - // Release the lock before removing the node to avoid deadlocks - nodeLock->release(); + // Release the lock before removing the node to avoid deadlocks + nodeLock->release(); - // Remove from graph - return resourceGraph_.removeNode(resourceId); - } + // Remove from graph + return resourceGraph_.removeNode(resourceId); + } } -bool ResourceHub::unload(const std::string &resourceId) { - return unload(resourceId, false); +bool ResourceHub::unload(const std::string& resourceId) { + return unload(resourceId, false); } -bool ResourceHub::unloadRecursive(const std::string &resourceId) { - // Get dependencies in topological order to ensure proper unloading - std::vector unloadOrder; - - // Helper function for DFS traversal - std::function &)> - collectDependents; - collectDependents = [&](const std::string &id, - std::unordered_set &visited) { - visited.insert(id); - - // First recurse to all dependents - auto dependents = resourceGraph_.getInEdges(id); - for (const auto &dependent : dependents) { - if (visited.find(dependent) == visited.end()) { - collectDependents(dependent, visited); - } - } +bool ResourceHub::unloadRecursive(const std::string& resourceId) { + // Get dependencies in topological order to ensure proper unloading + std::vector unloadOrder; + + // Helper function for DFS traversal + std::function&)> collectDependents; + collectDependents = [&](const std::string& id, std::unordered_set& visited) { + visited.insert(id); - // Then add this resource - unloadOrder.push_back(id); - }; - - // Collect dependents of this resource - std::unordered_set visited; - collectDependents(resourceId, visited); - - // Unload in topological order - bool success = true; - for (const auto &id : unloadOrder) { - auto node = resourceGraph_.getNode(id); - if (node) { - auto nodeLock = resourceGraph_.tryLockNode( - id, - CoordinatedGraph>::LockIntent::NodeModify, - true); - if (nodeLock && nodeLock->isLocked()) { - auto res = nodeLock->getNode()->getDataNoLock(); - if (res->getState() == ResourceState::Loaded) { - res->unload(); + // First recurse to all dependents + auto dependents = resourceGraph_.getInEdges(id); + for (const auto& dependent : dependents) { + if (visited.find(dependent) == visited.end()) { + collectDependents(dependent, visited); + } + } + + // Then add this resource + unloadOrder.push_back(id); + }; + + // Collect dependents of this resource + std::unordered_set visited; + collectDependents(resourceId, visited); + + // Unload in topological order + bool success = true; + for (const auto& id : unloadOrder) { + auto node = resourceGraph_.getNode(id); + if (node) { + auto nodeLock = resourceGraph_.tryLockNode( + id, CoordinatedGraph>::LockIntent::NodeModify, true); + if (nodeLock && nodeLock->isLocked()) { + auto res = nodeLock->getNode()->getDataNoLock(); + if (res->getState() == ResourceState::Loaded) { + res->unload(); + } + nodeLock->release(); + success &= resourceGraph_.removeNode(id); + } } - nodeLock->release(); - success &= resourceGraph_.removeNode(id); - } } - } - return success; + return success; } -void ResourceHub::preload(const std::vector &typeIds, - const std::vector &resourceIds, +void ResourceHub::preload(const std::vector& typeIds, const std::vector& resourceIds, ResourcePriority priority) { - if (typeIds.size() != resourceIds.size()) { - throw std::invalid_argument( - "typeIds and resourceIds must have the same size"); - } - - for (size_t i = 0; i < resourceIds.size(); ++i) { - ResourceLoadRequest request; - request.typeId = typeIds[i]; - request.resourceId = resourceIds[i]; - request.priority = priority; - - // Add the request to the queue - { - std::lock_guard lock(queueMutex_); - loadQueue_.push(request); + if (typeIds.size() != resourceIds.size()) { + throw std::invalid_argument("typeIds and resourceIds must have the same size"); + } + + for (size_t i = 0; i < resourceIds.size(); ++i) { + ResourceLoadRequest request; + request.typeId = typeIds[i]; + request.resourceId = resourceIds[i]; + request.priority = priority; + + // Add the request to the queue + { + std::lock_guard lock(queueMutex_); + loadQueue_.push(request); + } } - } - // Signal the worker thread - queueCondition_.notify_one(); + // Signal the worker thread + queueCondition_.notify_one(); } void ResourceHub::setMemoryBudget(size_t bytes) { - memoryBudget_ = bytes; - // When we set a new budget, check if we need to enforce it - enforceBudget(); + memoryBudget_ = bytes; + // When we set a new budget, check if we need to enforce it + enforceBudget(); } size_t ResourceHub::getMemoryUsage() const { - size_t total = 0; + size_t total = 0; - // Get all resources from the graph - auto allResourceIds = resourceGraph_.getAllNodes(); + // Get all resources from the graph + auto allResourceIds = resourceGraph_.getAllNodes(); - for (const auto &id : allResourceIds) { - auto node = resourceGraph_.getNode(id); - if (!node) { - continue; - } + for (const auto& id : allResourceIds) { + auto node = resourceGraph_.getNode(id); + if (!node) { + continue; + } - auto nodeLock = resourceGraph_.tryLockNode( - id, - CoordinatedGraph>::LockIntent::Read); - if (!nodeLock || !nodeLock->isLocked()) { - continue; - } + auto nodeLock = resourceGraph_.tryLockNode(id, CoordinatedGraph>::LockIntent::Read); + if (!nodeLock || !nodeLock->isLocked()) { + continue; + } - auto resource = nodeLock->getNode()->getDataNoLock(); - if (resource->getState() == ResourceState::Loaded) { - total += resource->getMemoryUsage(); + auto resource = nodeLock->getNode()->getDataNoLock(); + if (resource->getState() == ResourceState::Loaded) { + total += resource->getMemoryUsage(); + } } - } - return total; + return total; } -size_t ResourceHub::getMemoryBudget() const { return memoryBudget_; } - -bool ResourceHub::isLoaded(const std::string &resourceId) const { - // Simplified implementation with proper RAII for lock management - // and cleaner timeout protection - constexpr int TIMEOUT_MS = 50; // Short timeout for a read-only operation - - try { - // First check if the node exists - if (!resourceGraph_.hasNode(resourceId)) { - return false; - } - - // Get a shared pointer to the node - auto node = resourceGraph_.getNode(resourceId, TIMEOUT_MS); - if (!node) { - // Node couldn't be retrieved with timeout - return false; - } - - // Try to lock the node with a read intent - auto nodeLock = node->tryLock( - CoordinatedGraph>::LockIntent::Read, - TIMEOUT_MS); - - // If the lock couldn't be acquired, the resource isn't accessible - if (!nodeLock || !nodeLock->isLocked()) { - return false; - } - - // Use RAII to ensure the lock is released - // by creating a scope and releasing at the end - { - auto resource = nodeLock->getNode()->getDataNoLock(); - if (!resource) { - nodeLock->release(); +size_t ResourceHub::getMemoryBudget() const { + return memoryBudget_; +} + +bool ResourceHub::isLoaded(const std::string& resourceId) const { + // Simplified implementation with proper RAII for lock management + // and cleaner timeout protection + constexpr int TIMEOUT_MS = 50; // Short timeout for a read-only operation + + try { + // First check if the node exists + if (!resourceGraph_.hasNode(resourceId)) { + return false; + } + + // Get a shared pointer to the node + auto node = resourceGraph_.getNode(resourceId, TIMEOUT_MS); + if (!node) { + // Node couldn't be retrieved with timeout + return false; + } + + // Try to lock the node with a read intent + auto nodeLock = node->tryLock(CoordinatedGraph>::LockIntent::Read, TIMEOUT_MS); + + // If the lock couldn't be acquired, the resource isn't accessible + if (!nodeLock || !nodeLock->isLocked()) { + return false; + } + + // Use RAII to ensure the lock is released + // by creating a scope and releasing at the end + { + auto resource = nodeLock->getNode()->getDataNoLock(); + if (!resource) { + nodeLock->release(); + return false; + } + + ResourceState state = resource->getState(); + + // Release the lock + nodeLock->release(); + + // Return the loaded state + return state == ResourceState::Loaded; + } + } catch (const std::exception& e) { + // Log error but don't propagate exception + FABRIC_LOG_ERROR("Exception in isLoaded for {}: {}", resourceId, e.what()); + return false; + } catch (...) { + // Catch any other exceptions + FABRIC_LOG_ERROR("Unknown exception in isLoaded for {}", resourceId); return false; - } - - ResourceState state = resource->getState(); - - // Release the lock - nodeLock->release(); - - // Return the loaded state - return state == ResourceState::Loaded; } - } catch (const std::exception& e) { - // Log error but don't propagate exception - FABRIC_LOG_ERROR("Exception in isLoaded for {}: {}", resourceId, e.what()); - return false; - } catch (...) { - // Catch any other exceptions - FABRIC_LOG_ERROR("Unknown exception in isLoaded for {}", resourceId); - return false; - } } -std::vector -ResourceHub::getDependentResources(const std::string &resourceId) const { - auto dependents = resourceGraph_.getInEdges(resourceId); - return std::vector(dependents.begin(), dependents.end()); +std::vector ResourceHub::getDependentResources(const std::string& resourceId) const { + auto dependents = resourceGraph_.getInEdges(resourceId); + return std::vector(dependents.begin(), dependents.end()); } -std::vector -ResourceHub::getDependencyResources(const std::string &resourceId) const { - auto dependencies = resourceGraph_.getOutEdges(resourceId); - return std::vector(dependencies.begin(), dependencies.end()); +std::vector ResourceHub::getDependencyResources(const std::string& resourceId) const { + auto dependencies = resourceGraph_.getOutEdges(resourceId); + return std::vector(dependencies.begin(), dependencies.end()); } -std::unordered_set -ResourceHub::getDependents(const std::string &resourceId) { - return resourceGraph_.getInEdges(resourceId); +std::unordered_set ResourceHub::getDependents(const std::string& resourceId) { + return resourceGraph_.getInEdges(resourceId); } -std::unordered_set -ResourceHub::getDependencies(const std::string &resourceId) { - return resourceGraph_.getOutEdges(resourceId); +std::unordered_set ResourceHub::getDependencies(const std::string& resourceId) { + return resourceGraph_.getOutEdges(resourceId); } -bool ResourceHub::hasResource(const std::string &resourceId) { - return resourceGraph_.hasNode(resourceId); +bool ResourceHub::hasResource(const std::string& resourceId) { + return resourceGraph_.hasNode(resourceId); } size_t ResourceHub::enforceMemoryBudget() { - // Simplified implementation based on Copy-Then-Process pattern from IMPLEMENTATION_PATTERNS.md - - // Single mutex for budget enforcement with try_lock to prevent contention - static std::timed_mutex enforceBudgetMutex; - - // Try to acquire the lock without blocking - if (!enforceBudgetMutex.try_lock()) { - // Another thread is already enforcing the budget, skip this invocation - return 0; - } - - // Use RAII for mutex management - std::lock_guard budgetLockGuard(enforceBudgetMutex, std::adopt_lock); - - // Constants for timeout protection - constexpr int ENFORCE_TIMEOUT_MS = 300; // 300ms total timeout - constexpr int NODE_TIMEOUT_MS = 25; // 25ms per node operation timeout - - // Start the timeout timer - auto startTime = std::chrono::steady_clock::now(); - - // Timeout checker function - auto isTimedOut = [&startTime, ENFORCE_TIMEOUT_MS]() -> bool { - return std::chrono::duration_cast( - std::chrono::steady_clock::now() - startTime) - .count() > ENFORCE_TIMEOUT_MS; - }; - - // Check if we need to enforce the budget - size_t currentUsage = 0; - try { - currentUsage = getMemoryUsage(); - - if (currentUsage <= memoryBudget_) { - return 0; // No need to enforce budget - } - } catch (const std::exception& e) { - FABRIC_LOG_ERROR("Exception in getMemoryUsage: {}", e.what()); - return 0; - } - - // Calculate how much memory we need to free - size_t toFree = currentUsage - memoryBudget_; - - // Get all resource IDs once and copy - std::vector allResourceIds; - try { - allResourceIds = resourceGraph_.getAllNodes(); - } catch (const std::exception& e) { - FABRIC_LOG_ERROR("Failed to get all nodes: {}", e.what()); - return 0; - } - - // ================================================================= - // Phase 1: Collect candidates (using the Copy-Then-Process pattern) - // ================================================================= - - // Define an eviction candidate structure - struct EvictionCandidate { - std::string id; - std::chrono::steady_clock::time_point lastAccessTime; - size_t size; - bool hasDependents; - }; - - std::vector candidates; - candidates.reserve(allResourceIds.size()); - - // Collect initial candidate information with minimal locking - for (const auto& id : allResourceIds) { - if (isTimedOut()) { - FABRIC_LOG_WARN("enforceMemoryBudget timed out during candidate collection"); - return 0; + // Simplified implementation based on Copy-Then-Process pattern from IMPLEMENTATION_PATTERNS.md + + // Single mutex for budget enforcement with try_lock to prevent contention + static std::timed_mutex enforceBudgetMutex; + + // Try to acquire the lock without blocking + if (!enforceBudgetMutex.try_lock()) { + // Another thread is already enforcing the budget, skip this invocation + return 0; } - - // Don't waste time on nodes that have dependents - bool hasDependents = false; + + // Use RAII for mutex management + std::lock_guard budgetLockGuard(enforceBudgetMutex, std::adopt_lock); + + // Constants for timeout protection + constexpr int ENFORCE_TIMEOUT_MS = 300; // 300ms total timeout + constexpr int NODE_TIMEOUT_MS = 25; // 25ms per node operation timeout + + // Start the timeout timer + auto startTime = std::chrono::steady_clock::now(); + + // Timeout checker function + auto isTimedOut = [&startTime, ENFORCE_TIMEOUT_MS]() -> bool { + return std::chrono::duration_cast(std::chrono::steady_clock::now() - startTime) + .count() > ENFORCE_TIMEOUT_MS; + }; + + // Check if we need to enforce the budget + size_t currentUsage = 0; try { - auto dependents = resourceGraph_.getInEdges(id); - hasDependents = !dependents.empty(); - - if (hasDependents) { - continue; // Skip resources with dependents - } + currentUsage = getMemoryUsage(); + + if (currentUsage <= memoryBudget_) { + return 0; // No need to enforce budget + } } catch (const std::exception& e) { - // Skip if we can't check dependencies - continue; - } - - // Get node info with minimal locking - auto node = resourceGraph_.getNode(id, NODE_TIMEOUT_MS); - if (!node) { - continue; + FABRIC_LOG_ERROR("Exception in getMemoryUsage: {}", e.what()); + return 0; } - - // Get node data with read lock - auto nodeLock = node->tryLock( - CoordinatedGraph>::LockIntent::Read, - NODE_TIMEOUT_MS); - - if (!nodeLock || !nodeLock->isLocked()) { - continue; - } - - // Gather resource information - std::shared_ptr resource; - size_t resourceSize = 0; - std::chrono::steady_clock::time_point lastAccess; - bool isLoaded = false; - bool hasSingleRef = false; - + + // Calculate how much memory we need to free + size_t toFree = currentUsage - memoryBudget_; + + // Get all resource IDs once and copy + std::vector allResourceIds; try { - resource = nodeLock->getNode()->getDataNoLock(); - if (resource) { - resourceSize = resource->getMemoryUsage(); - lastAccess = node->getLastAccessTime(); - isLoaded = resource->getState() == ResourceState::Loaded; - hasSingleRef = resource.use_count() == 1; - } + allResourceIds = resourceGraph_.getAllNodes(); } catch (const std::exception& e) { - FABRIC_LOG_DEBUG("ResourceHub: skipping candidate '{}': {}", id, e.what()); + FABRIC_LOG_ERROR("Failed to get all nodes: {}", e.what()); + return 0; } - - // Release lock immediately - nodeLock->release(); - - // Add to candidates if it meets criteria - if (resource && isLoaded && hasSingleRef && !hasDependents) { - candidates.push_back({id, lastAccess, resourceSize, hasDependents}); + + // ================================================================= + // Phase 1: Collect candidates (using the Copy-Then-Process pattern) + // ================================================================= + + // Define an eviction candidate structure + struct EvictionCandidate { + std::string id; + std::chrono::steady_clock::time_point lastAccessTime; + size_t size; + bool hasDependents; + }; + + std::vector candidates; + candidates.reserve(allResourceIds.size()); + + // Collect initial candidate information with minimal locking + for (const auto& id : allResourceIds) { + if (isTimedOut()) { + FABRIC_LOG_WARN("enforceMemoryBudget timed out during candidate collection"); + return 0; + } + + // Don't waste time on nodes that have dependents + bool hasDependents = false; + try { + auto dependents = resourceGraph_.getInEdges(id); + hasDependents = !dependents.empty(); + + if (hasDependents) { + continue; // Skip resources with dependents + } + } catch (const std::exception& e) { + // Skip if we can't check dependencies + continue; + } + + // Get node info with minimal locking + auto node = resourceGraph_.getNode(id, NODE_TIMEOUT_MS); + if (!node) { + continue; + } + + // Get node data with read lock + auto nodeLock = node->tryLock(CoordinatedGraph>::LockIntent::Read, NODE_TIMEOUT_MS); + + if (!nodeLock || !nodeLock->isLocked()) { + continue; + } + + // Gather resource information + std::shared_ptr resource; + size_t resourceSize = 0; + std::chrono::steady_clock::time_point lastAccess; + bool isLoaded = false; + bool hasSingleRef = false; + + try { + resource = nodeLock->getNode()->getDataNoLock(); + if (resource) { + resourceSize = resource->getMemoryUsage(); + lastAccess = node->getLastAccessTime(); + isLoaded = resource->getState() == ResourceState::Loaded; + hasSingleRef = resource.use_count() == 1; + } + } catch (const std::exception& e) { + FABRIC_LOG_DEBUG("ResourceHub: skipping candidate '{}': {}", id, e.what()); + } + + // Release lock immediately + nodeLock->release(); + + // Add to candidates if it meets criteria + if (resource && isLoaded && hasSingleRef && !hasDependents) { + candidates.push_back({id, lastAccess, resourceSize, hasDependents}); + } } - } - - // Check if we have any candidates - if (candidates.empty() || isTimedOut()) { - return 0; - } - - // ================================================================= - // Phase 2: Sort candidates by last access time (oldest first) - // ================================================================= - try { - std::sort(candidates.begin(), candidates.end(), - [](const EvictionCandidate& a, const EvictionCandidate& b) { - return a.lastAccessTime < b.lastAccessTime; - }); - } catch (const std::exception& e) { - FABRIC_LOG_ERROR("Exception sorting candidates: {}", e.what()); - return 0; - } - - // ================================================================= - // Phase 3: Evict resources until we've freed enough memory - // ================================================================= - size_t evictedCount = 0; - size_t freedMemory = 0; - - for (const auto& candidate : candidates) { - if (isTimedOut()) { - FABRIC_LOG_WARN("enforceMemoryBudget timed out during eviction"); - break; + + // Check if we have any candidates + if (candidates.empty() || isTimedOut()) { + return 0; } - - // Double-check dependencies with minimal lock + + // ================================================================= + // Phase 2: Sort candidates by last access time (oldest first) + // ================================================================= try { - auto dependents = resourceGraph_.getInEdges(candidate.id); - if (!dependents.empty()) { - continue; // Skip if it has dependents now - } + std::sort(candidates.begin(), candidates.end(), [](const EvictionCandidate& a, const EvictionCandidate& b) { + return a.lastAccessTime < b.lastAccessTime; + }); } catch (const std::exception& e) { - continue; // Skip if we can't check dependencies - } - - // Get node with write lock - auto nodeLock = resourceGraph_.tryLockNode( - candidate.id, - CoordinatedGraph>::LockIntent::NodeModify, - true, NODE_TIMEOUT_MS); - - if (!nodeLock || !nodeLock->isLocked()) { - continue; + FABRIC_LOG_ERROR("Exception sorting candidates: {}", e.what()); + return 0; } - - // Get resource and verify it's still evictable - std::shared_ptr resource; - - try { - resource = nodeLock->getNode()->getDataNoLock(); - // Double-check conditions under lock - if (!resource || resource.use_count() > 1 || - resource->getState() != ResourceState::Loaded) { - nodeLock->release(); - continue; - } - - // Unload the resource - resource->unload(); - - // Update access time - nodeLock->getNode()->touch(); - - // Release the lock - nodeLock->release(); - - // Remove from graph now that it's unloaded - bool removed = resourceGraph_.removeNode(candidate.id); - - if (removed) { - // Update stats - freedMemory += candidate.size; - evictedCount++; - - // Log success - FABRIC_LOG_DEBUG("Evicted resource: {}", candidate.id); - } - } catch (const std::exception& e) { - // Make sure lock is released on exception - try { - if (nodeLock->isLocked()) { - nodeLock->release(); - } - } catch (...) { - FABRIC_LOG_WARN("ResourceHub: failed to release lock for '{}'", candidate.id); - } - - FABRIC_LOG_ERROR("Error evicting resource {}: {}", candidate.id, e.what()); - continue; - } - - // If we've freed enough memory, we can stop - if (freedMemory >= toFree) { - break; - } - } - - return evictedCount; -} + // ================================================================= + // Phase 3: Evict resources until we've freed enough memory + // ================================================================= + size_t evictedCount = 0; + size_t freedMemory = 0; -void ResourceHub::processLoadQueue() { - try { - while (true) { - ResourceLoadRequest request; - bool hasRequest = false; - - // Get a request from the queue - { - std::unique_lock lock(queueMutex_); - - // Wait for a request or shutdown signal - // Add a timeout to periodically check the shutdown status even if the - // condition variable isn't notified - auto waitResult = queueCondition_.wait_for( - lock, std::chrono::milliseconds(500), - [this] { return !loadQueue_.empty() || shutdown_; }); - - // Check for shutdown first - if (shutdown_) { - break; + for (const auto& candidate : candidates) { + if (isTimedOut()) { + FABRIC_LOG_WARN("enforceMemoryBudget timed out during eviction"); + break; } - // Skip this iteration if no request is available - if (!waitResult || loadQueue_.empty()) { - continue; + // Double-check dependencies with minimal lock + try { + auto dependents = resourceGraph_.getInEdges(candidate.id); + if (!dependents.empty()) { + continue; // Skip if it has dependents now + } + } catch (const std::exception& e) { + continue; // Skip if we can't check dependencies } - // Get the next request - request = loadQueue_.top(); - loadQueue_.pop(); - hasRequest = true; - } + // Get node with write lock + auto nodeLock = resourceGraph_.tryLockNode( + candidate.id, CoordinatedGraph>::LockIntent::NodeModify, true, NODE_TIMEOUT_MS); - // Skip processing if we didn't get a valid request - if (!hasRequest) { - continue; - } + if (!nodeLock || !nodeLock->isLocked()) { + continue; + } - try { - // Process the request - auto resourceNode = resourceGraph_.getNode(request.resourceId); + // Get resource and verify it's still evictable std::shared_ptr resource; - if (!resourceNode) { - // Create a new resource - resource = - ResourceFactory::create(request.typeId, request.resourceId); - if (resource) { - if (!resourceGraph_.addNode(request.resourceId, resource)) { - // Node may have been added by another thread, try to get it again - resourceNode = resourceGraph_.getNode(request.resourceId); - if (resourceNode) { - auto nodeLock = resourceNode->tryLock( - CoordinatedGraph< - std::shared_ptr>::LockIntent::Read); - - if (nodeLock && nodeLock->isLocked()) { - resource = nodeLock->getNode()->getDataNoLock(); - } - } - } else { - resourceNode = resourceGraph_.getNode(request.resourceId); + try { + resource = nodeLock->getNode()->getDataNoLock(); + + // Double-check conditions under lock + if (!resource || resource.use_count() > 1 || resource->getState() != ResourceState::Loaded) { + nodeLock->release(); + continue; } - } - } else { - auto nodeLock = resourceNode->tryLock( - CoordinatedGraph>::LockIntent::Read); - if (nodeLock && nodeLock->isLocked()) { - resource = nodeLock->getNode()->getDataNoLock(); - } - } + // Unload the resource + resource->unload(); + + // Update access time + nodeLock->getNode()->touch(); + + // Release the lock + nodeLock->release(); + + // Remove from graph now that it's unloaded + bool removed = resourceGraph_.removeNode(candidate.id); - // Load the resource if needed - if (resource) { - ResourceState state = resource->getState(); - if (state != ResourceState::Loaded) { - // Try to load - handle exceptions that might occur + if (removed) { + // Update stats + freedMemory += candidate.size; + evictedCount++; + + // Log success + FABRIC_LOG_DEBUG("Evicted resource: {}", candidate.id); + } + } catch (const std::exception& e) { + // Make sure lock is released on exception try { - resource->load(); - - // Update the last access time - if (resourceNode) { - resourceNode->touch(); - } - } catch (const std::exception &e) { - FABRIC_LOG_ERROR("Error loading resource {}: {}", request.resourceId, e.what()); + if (nodeLock->isLocked()) { + nodeLock->release(); + } + } catch (...) { + FABRIC_LOG_WARN("ResourceHub: failed to release lock for '{}'", candidate.id); } - } + + FABRIC_LOG_ERROR("Error evicting resource {}: {}", candidate.id, e.what()); + continue; } - // Enforce memory budget - handle any exceptions - try { - enforceBudget(); - } catch (const std::exception &e) { - FABRIC_LOG_ERROR("Error enforcing memory budget: {}", e.what()); + // If we've freed enough memory, we can stop + if (freedMemory >= toFree) { + break; } + } + + return evictedCount; +} + +void ResourceHub::processLoadQueue() { + try { + while (true) { + ResourceLoadRequest request; + bool hasRequest = false; + + // Get a request from the queue + { + std::unique_lock lock(queueMutex_); + + // Wait for a request or shutdown signal + // Add a timeout to periodically check the shutdown status even if the + // condition variable isn't notified + auto waitResult = queueCondition_.wait_for(lock, std::chrono::milliseconds(500), + [this] { return !loadQueue_.empty() || shutdown_; }); + + // Check for shutdown first + if (shutdown_) { + break; + } + + // Skip this iteration if no request is available + if (!waitResult || loadQueue_.empty()) { + continue; + } + + // Get the next request + request = loadQueue_.top(); + loadQueue_.pop(); + hasRequest = true; + } + + // Skip processing if we didn't get a valid request + if (!hasRequest) { + continue; + } + + try { + // Process the request + auto resourceNode = resourceGraph_.getNode(request.resourceId); + std::shared_ptr resource; + + if (!resourceNode) { + // Create a new resource + resource = ResourceFactory::create(request.typeId, request.resourceId); + if (resource) { + if (!resourceGraph_.addNode(request.resourceId, resource)) { + // Node may have been added by another thread, try to get it again + resourceNode = resourceGraph_.getNode(request.resourceId); + if (resourceNode) { + auto nodeLock = resourceNode->tryLock( + CoordinatedGraph>::LockIntent::Read); + + if (nodeLock && nodeLock->isLocked()) { + resource = nodeLock->getNode()->getDataNoLock(); + } + } + } else { + resourceNode = resourceGraph_.getNode(request.resourceId); + } + } + } else { + auto nodeLock = + resourceNode->tryLock(CoordinatedGraph>::LockIntent::Read); + + if (nodeLock && nodeLock->isLocked()) { + resource = nodeLock->getNode()->getDataNoLock(); + } + } - // Call the callback - handle any exceptions - if (request.callback && resource) { - try { - request.callback(resource); - } catch (const std::exception &e) { - FABRIC_LOG_ERROR("Error in resource callback for {}: {}", request.resourceId, e.what()); - } + // Load the resource if needed + if (resource) { + ResourceState state = resource->getState(); + if (state != ResourceState::Loaded) { + // Try to load - handle exceptions that might occur + try { + resource->load(); + + // Update the last access time + if (resourceNode) { + resourceNode->touch(); + } + } catch (const std::exception& e) { + FABRIC_LOG_ERROR("Error loading resource {}: {}", request.resourceId, e.what()); + } + } + } + + // Enforce memory budget - handle any exceptions + try { + enforceBudget(); + } catch (const std::exception& e) { + FABRIC_LOG_ERROR("Error enforcing memory budget: {}", e.what()); + } + + // Call the callback - handle any exceptions + if (request.callback && resource) { + try { + request.callback(resource); + } catch (const std::exception& e) { + FABRIC_LOG_ERROR("Error in resource callback for {}: {}", request.resourceId, e.what()); + } + } + } catch (const std::exception& e) { + // Catch any exception during request processing to keep the thread + // alive + FABRIC_LOG_ERROR("Error processing request for {}: {}", request.resourceId, e.what()); + } catch (...) { + // Catch any unknown exception + FABRIC_LOG_ERROR("Unknown error processing request for {}", request.resourceId); + } } - } catch (const std::exception &e) { - // Catch any exception during request processing to keep the thread - // alive - FABRIC_LOG_ERROR("Error processing request for {}: {}", request.resourceId, e.what()); - } catch (...) { + } catch (const std::exception& e) { + // Log the error but don't propagate it - this would terminate the thread + FABRIC_LOG_CRITICAL("Fatal worker thread error: {}", e.what()); + } catch (...) { // Catch any unknown exception - FABRIC_LOG_ERROR("Unknown error processing request for {}", request.resourceId); - } + FABRIC_LOG_CRITICAL("Unknown fatal worker thread error"); } - } catch (const std::exception &e) { - // Log the error but don't propagate it - this would terminate the thread - FABRIC_LOG_CRITICAL("Fatal worker thread error: {}", e.what()); - } catch (...) { - // Catch any unknown exception - FABRIC_LOG_CRITICAL("Unknown fatal worker thread error"); - } } void ResourceHub::disableWorkerThreadsForTesting() { - // Set shutdown flag first (it's atomic and thread-safe) - shutdown_ = true; - - // Notify all threads to check the shutdown flag - queueCondition_.notify_all(); - - // Try to acquire mutex with a timeout - don't block indefinitely - auto timeoutMs = 100; - auto start = std::chrono::steady_clock::now(); - while (!threadControlMutex_.try_lock()) { - if (std::chrono::duration_cast( - std::chrono::steady_clock::now() - start).count() > timeoutMs) { - FABRIC_LOG_WARN("Could not acquire thread control mutex in disableWorkerThreadsForTesting"); - - // Even if we couldn't get the mutex, we've already set the shutdown flag - // and notified threads, so they should eventually terminate - workerThreads_.clear(); - workerThreadCount_ = 0; - return; - } - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - - // Use RAII for proper mutex management - std::lock_guard guard(threadControlMutex_, std::adopt_lock); - - // Return early if already shut down - if (workerThreadCount_ == 0 && workerThreads_.empty()) { - return; - } - - // Clear any pending requests to help blocked workers exit faster - { - // Try to acquire queue lock with timeout - auto queueLockTimeoutMs = 50; - auto queueLockStart = std::chrono::steady_clock::now(); - bool queueLockAcquired = false; - - while (!queueMutex_.try_lock()) { - if (std::chrono::duration_cast( - std::chrono::steady_clock::now() - queueLockStart).count() > queueLockTimeoutMs) { - break; - } - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - - if (queueMutex_.try_lock()) { - queueLockAcquired = true; - - // Clear the queue - while (!loadQueue_.empty()) { - loadQueue_.pop(); - } - } - - if (queueLockAcquired) { - queueMutex_.unlock(); - } - } - - // Notify all threads again - queueCondition_.notify_all(); - - // Non-blocking thread termination approach - auto terminateThreads = [this]() { - const int MAX_JOIN_TIME_MS = 200; // Maximum time to wait for all threads - auto joinStart = std::chrono::steady_clock::now(); - - // First attempt to join threads normally with a reasonable timeout - for (auto& thread : workerThreads_) { - if (thread && thread->joinable()) { - // Create a timeout for this specific join - auto joinThreadStart = std::chrono::steady_clock::now(); - - // Use a short timeout per thread - const int PER_THREAD_TIMEOUT_MS = 50; - - // Keep trying until timeout - while (std::chrono::duration_cast( - std::chrono::steady_clock::now() - joinThreadStart).count() < PER_THREAD_TIMEOUT_MS) { - - // Use try_join with a very small timeout to avoid blocking - std::thread joiner([&thread]() { - if (thread->joinable()) { - thread->join(); - } - }); - joiner.detach(); - - // Short sleep to give joiner a chance - std::this_thread::sleep_for(std::chrono::milliseconds(5)); - - // Check overall timeout - if (std::chrono::duration_cast( - std::chrono::steady_clock::now() - joinStart).count() > MAX_JOIN_TIME_MS) { - FABRIC_LOG_WARN("Thread termination timeout in disableWorkerThreadsForTesting"); + // Set shutdown flag first (it's atomic and thread-safe) + shutdown_ = true; + + // Notify all threads to check the shutdown flag + queueCondition_.notify_all(); + + // Try to acquire mutex with a timeout - don't block indefinitely + auto timeoutMs = 100; + auto start = std::chrono::steady_clock::now(); + while (!threadControlMutex_.try_lock()) { + if (std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count() > + timeoutMs) { + FABRIC_LOG_WARN("Could not acquire thread control mutex in disableWorkerThreadsForTesting"); + + // Even if we couldn't get the mutex, we've already set the shutdown flag + // and notified threads, so they should eventually terminate + workerThreads_.clear(); + workerThreadCount_ = 0; return; - } } - } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); } - }; - - // Run thread termination with timeout protection - std::thread terminationThread(terminateThreads); - terminationThread.detach(); // Don't wait for it to complete - - // Clear the worker threads vector regardless - threads will self-terminate - // due to the shutdown flag being set and queueCondition being notified - workerThreads_.clear(); - workerThreadCount_ = 0; - - // Log completion - FABRIC_LOG_DEBUG("Worker threads disabled for testing"); -} -void ResourceHub::restartWorkerThreadsAfterTesting() { - // Use mutex to synchronize thread creation - std::unique_lock qLock(threadControlMutex_); + // Use RAII for proper mutex management + std::lock_guard guard(threadControlMutex_, std::adopt_lock); + + // Return early if already shut down + if (workerThreadCount_ == 0 && workerThreads_.empty()) { + return; + } - // Make sure any existing threads are properly shut down - if (!workerThreads_.empty()) { - // Signal threads to exit + // Clear any pending requests to help blocked workers exit faster { - std::lock_guard qLock(queueMutex_); - shutdown_ = true; - // Clear any pending requests to prevent blocked workers - while (!loadQueue_.empty()) { - loadQueue_.pop(); - } + // Try to acquire queue lock with timeout + auto queueLockTimeoutMs = 50; + auto queueLockStart = std::chrono::steady_clock::now(); + bool queueLockAcquired = false; + + while (!queueMutex_.try_lock()) { + if (std::chrono::duration_cast(std::chrono::steady_clock::now() - queueLockStart) + .count() > queueLockTimeoutMs) { + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + if (queueMutex_.try_lock()) { + queueLockAcquired = true; + + // Clear the queue + while (!loadQueue_.empty()) { + loadQueue_.pop(); + } + } + + if (queueLockAcquired) { + queueMutex_.unlock(); + } } + + // Notify all threads again queueCondition_.notify_all(); - // Join with timeout to prevent hangs - for (auto &thread : workerThreads_) { - if (thread && thread->joinable()) { - // Create a timeout thread - std::atomic joinCompleted{false}; - std::thread timeoutThread([&]() { - for (int i = 0; i < 50; i++) { // Wait up to 5 seconds - if (joinCompleted) - return; - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - } - if (!joinCompleted) { - // Log timeout warning - FABRIC_LOG_WARN("Thread join timeout in restartWorkerThreadsAfterTesting"); - } - }); + // Non-blocking thread termination approach + auto terminateThreads = [this]() { + const int MAX_JOIN_TIME_MS = 200; // Maximum time to wait for all threads + auto joinStart = std::chrono::steady_clock::now(); + + // First attempt to join threads normally with a reasonable timeout + for (auto& thread : workerThreads_) { + if (thread && thread->joinable()) { + // Create a timeout for this specific join + auto joinThreadStart = std::chrono::steady_clock::now(); + + // Use a short timeout per thread + const int PER_THREAD_TIMEOUT_MS = 50; + + // Keep trying until timeout + while (std::chrono::duration_cast(std::chrono::steady_clock::now() - + joinThreadStart) + .count() < PER_THREAD_TIMEOUT_MS) { + + // Use try_join with a very small timeout to avoid blocking + std::thread joiner([&thread]() { + if (thread->joinable()) { + thread->join(); + } + }); + joiner.detach(); + + // Short sleep to give joiner a chance + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + + // Check overall timeout + if (std::chrono::duration_cast(std::chrono::steady_clock::now() - + joinStart) + .count() > MAX_JOIN_TIME_MS) { + FABRIC_LOG_WARN("Thread termination timeout in disableWorkerThreadsForTesting"); + return; + } + } + } + } + }; - // Join the worker thread - thread->join(); - joinCompleted = true; + // Run thread termination with timeout protection + std::thread terminationThread(terminateThreads); + terminationThread.detach(); // Don't wait for it to complete + + // Clear the worker threads vector regardless - threads will self-terminate + // due to the shutdown flag being set and queueCondition being notified + workerThreads_.clear(); + workerThreadCount_ = 0; - // Clean up timeout thread - if (timeoutThread.joinable()) { - timeoutThread.join(); + // Log completion + FABRIC_LOG_DEBUG("Worker threads disabled for testing"); +} + +void ResourceHub::restartWorkerThreadsAfterTesting() { + // Use mutex to synchronize thread creation + std::unique_lock qLock(threadControlMutex_); + + // Make sure any existing threads are properly shut down + if (!workerThreads_.empty()) { + // Signal threads to exit + { + std::lock_guard qLock(queueMutex_); + shutdown_ = true; + // Clear any pending requests to prevent blocked workers + while (!loadQueue_.empty()) { + loadQueue_.pop(); + } } - } + queueCondition_.notify_all(); + + // Join with timeout to prevent hangs + for (auto& thread : workerThreads_) { + if (thread && thread->joinable()) { + // Create a timeout thread + std::atomic joinCompleted{false}; + std::thread timeoutThread([&]() { + for (int i = 0; i < 50; i++) { // Wait up to 5 seconds + if (joinCompleted) + return; + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + if (!joinCompleted) { + // Log timeout warning + FABRIC_LOG_WARN("Thread join timeout in restartWorkerThreadsAfterTesting"); + } + }); + + // Join the worker thread + thread->join(); + joinCompleted = true; + + // Clean up timeout thread + if (timeoutThread.joinable()) { + timeoutThread.join(); + } + } + } + workerThreads_.clear(); + } + + // Start fresh + shutdown_ = false; + workerThreadCount_ = std::thread::hardware_concurrency(); + + // Start worker threads + workerThreads_.reserve(workerThreadCount_); + for (unsigned int i = 0; i < workerThreadCount_; ++i) { + workerThreads_.push_back(std::make_unique(&ResourceHub::workerThreadFunc, this)); } - workerThreads_.clear(); - } - - // Start fresh - shutdown_ = false; - workerThreadCount_ = std::thread::hardware_concurrency(); - - // Start worker threads - workerThreads_.reserve(workerThreadCount_); - for (unsigned int i = 0; i < workerThreadCount_; ++i) { - workerThreads_.push_back( - std::make_unique(&ResourceHub::workerThreadFunc, this)); - } } unsigned int ResourceHub::getWorkerThreadCount() const { - return workerThreadCount_; + return workerThreadCount_; } void ResourceHub::setWorkerThreadCount(unsigned int count) { - if (count == 0) { - throw std::invalid_argument("Worker thread count must be at least 1"); - } - - // Use thread control mutex to synchronize thread adjustments - std::unique_lock controlLock(threadControlMutex_); - - // If no change, return early - if (count == workerThreadCount_) { - return; - } - - // If decreasing thread count, signal specific threads to stop - if (count < workerThreadCount_) { - unsigned int threadsToStop = workerThreadCount_ - count; - std::vector> threadsToJoin; - - // Move threads to be stopped to a separate vector - for (unsigned int i = 0; i < threadsToStop; ++i) { - if (i < workerThreads_.size()) { - threadsToJoin.push_back(std::move(workerThreads_.back())); - workerThreads_.pop_back(); - } + if (count == 0) { + throw std::invalid_argument("Worker thread count must be at least 1"); } - // Signal threads to check their shutdown status - queueCondition_.notify_all(); + // Use thread control mutex to synchronize thread adjustments + std::unique_lock controlLock(threadControlMutex_); - // Join the threads we're removing with timeout protection - for (auto &thread : threadsToJoin) { - if (thread && thread->joinable()) { - // Create a timeout thread - std::atomic joinCompleted{false}; - std::thread timeoutThread([&]() { - for (int i = 0; i < 30; i++) { // 3 second timeout - if (joinCompleted) - return; - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - } - if (!joinCompleted) { - // Log timeout warning - FABRIC_LOG_WARN("Thread join timeout in setWorkerThreadCount"); - } - }); + // If no change, return early + if (count == workerThreadCount_) { + return; + } - // Join the worker thread - thread->join(); - joinCompleted = true; + // If decreasing thread count, signal specific threads to stop + if (count < workerThreadCount_) { + unsigned int threadsToStop = workerThreadCount_ - count; + std::vector> threadsToJoin; - // Clean up timeout thread - if (timeoutThread.joinable()) { - timeoutThread.join(); + // Move threads to be stopped to a separate vector + for (unsigned int i = 0; i < threadsToStop; ++i) { + if (i < workerThreads_.size()) { + threadsToJoin.push_back(std::move(workerThreads_.back())); + workerThreads_.pop_back(); + } } - } - } - } - // If increasing thread count, create new threads - if (count > workerThreadCount_) { - // Ensure we're not in shutdown state - if (shutdown_) { - shutdown_ = false; // Reset shutdown flag + // Signal threads to check their shutdown status + queueCondition_.notify_all(); + + // Join the threads we're removing with timeout protection + for (auto& thread : threadsToJoin) { + if (thread && thread->joinable()) { + // Create a timeout thread + std::atomic joinCompleted{false}; + std::thread timeoutThread([&]() { + for (int i = 0; i < 30; i++) { // 3 second timeout + if (joinCompleted) + return; + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + if (!joinCompleted) { + // Log timeout warning + FABRIC_LOG_WARN("Thread join timeout in setWorkerThreadCount"); + } + }); + + // Join the worker thread + thread->join(); + joinCompleted = true; + + // Clean up timeout thread + if (timeoutThread.joinable()) { + timeoutThread.join(); + } + } + } } - // Create new threads - unsigned int threadsToAdd = count - workerThreadCount_; - workerThreads_.reserve(workerThreads_.size() + threadsToAdd); - - for (unsigned int i = 0; i < threadsToAdd; ++i) { - try { - workerThreads_.push_back(std::make_unique( - &ResourceHub::workerThreadFunc, this)); - } catch (const std::exception &e) { - // Log thread creation error - FABRIC_LOG_ERROR("Error creating worker thread: {}", e.what()); - } + // If increasing thread count, create new threads + if (count > workerThreadCount_) { + // Ensure we're not in shutdown state + if (shutdown_) { + shutdown_ = false; // Reset shutdown flag + } + + // Create new threads + unsigned int threadsToAdd = count - workerThreadCount_; + workerThreads_.reserve(workerThreads_.size() + threadsToAdd); + + for (unsigned int i = 0; i < threadsToAdd; ++i) { + try { + workerThreads_.push_back(std::make_unique(&ResourceHub::workerThreadFunc, this)); + } catch (const std::exception& e) { + // Log thread creation error + FABRIC_LOG_ERROR("Error creating worker thread: {}", e.what()); + } + } } - } - // Update the thread count - workerThreadCount_ = count; + // Update the thread count + workerThreadCount_ = count; } // Constructor implementation @@ -1127,191 +1104,184 @@ ResourceHub::ResourceHub() : memoryBudget_(1024 * 1024 * 1024), // 1 GB default workerThreadCount_(std::thread::hardware_concurrency()), shutdown_(false) { - // Optional debug message but quieter - FABRIC_LOG_DEBUG("ResourceHub initialized with {} configured worker threads", - workerThreadCount_.load()); - - // Don't start threads here - let them be started explicitly - // This prevents issues with tests - - // Detect if we're in a test environment - bool inTestEnvironment = true; - try { - // Check for a common environment variable that testing frameworks set - // or try to detect common test patterns in the executable path - char* testEnv = std::getenv("GTEST_ALSO_RUN_DISABLED_TESTS"); - if (testEnv != nullptr) { - inTestEnvironment = true; - } else { - // Try to get the executable path - std::array buffer; - std::fill(buffer.begin(), buffer.end(), 0); - - // Use platform-specific way to get executable path + // Optional debug message but quieter + FABRIC_LOG_DEBUG("ResourceHub initialized with {} configured worker threads", workerThreadCount_.load()); + + // Don't start threads here - let them be started explicitly + // This prevents issues with tests + + // Detect if we're in a test environment + bool inTestEnvironment = true; + try { + // Check for a common environment variable that testing frameworks set + // or try to detect common test patterns in the executable path + char* testEnv = std::getenv("GTEST_ALSO_RUN_DISABLED_TESTS"); + if (testEnv != nullptr) { + inTestEnvironment = true; + } else { + // Try to get the executable path + std::array buffer; + std::fill(buffer.begin(), buffer.end(), 0); + + // Use platform-specific way to get executable path #if defined(_WIN32) - GetModuleFileNameA(NULL, buffer.data(), buffer.size()); + GetModuleFileNameA(NULL, buffer.data(), buffer.size()); #elif defined(__APPLE__) - uint32_t size = buffer.size(); - if (_NSGetExecutablePath(buffer.data(), &size) != 0) { - buffer[0] = 0; - } + uint32_t size = buffer.size(); + if (_NSGetExecutablePath(buffer.data(), &size) != 0) { + buffer[0] = 0; + } #elif defined(__linux__) - char procPath[32]; - sprintf(procPath, "/proc/%d/exe", getpid()); - if (readlink(procPath, buffer.data(), buffer.size()) == -1) { - buffer[0] = 0; - } + char procPath[32]; + sprintf(procPath, "/proc/%d/exe", getpid()); + if (readlink(procPath, buffer.data(), buffer.size()) == -1) { + buffer[0] = 0; + } #endif - - std::string exePath(buffer.data()); - // If the path contains "test", "Test", "TEST", assume it's a test - std::string lowerPath = exePath; - std::transform(lowerPath.begin(), lowerPath.end(), lowerPath.begin(), - [](unsigned char c){ return std::tolower(c); }); - - if (lowerPath.find("test") != std::string::npos || - lowerPath.find("unittest") != std::string::npos) { + + std::string exePath(buffer.data()); + // If the path contains "test", "Test", "TEST", assume it's a test + std::string lowerPath = exePath; + std::transform(lowerPath.begin(), lowerPath.end(), lowerPath.begin(), + [](unsigned char c) { return std::tolower(c); }); + + if (lowerPath.find("test") != std::string::npos || lowerPath.find("unittest") != std::string::npos) { + inTestEnvironment = true; + } else { + inTestEnvironment = false; + } + } + } catch (...) { + FABRIC_LOG_DEBUG("ResourceHub: exception during test environment detection, defaulting to test mode"); inTestEnvironment = true; - } else { - inTestEnvironment = false; - } } - } catch (...) { - FABRIC_LOG_DEBUG("ResourceHub: exception during test environment detection, defaulting to test mode"); - inTestEnvironment = true; - } - - // Only start worker threads if we're not in a test environment - if (!inTestEnvironment && workerThreadCount_ > 0) { - FABRIC_LOG_INFO("Starting {} worker threads", workerThreadCount_.load()); - for (unsigned int i = 0; i < workerThreadCount_; ++i) { - workerThreads_.push_back( - std::make_unique(&ResourceHub::workerThreadFunc, this)); + + // Only start worker threads if we're not in a test environment + if (!inTestEnvironment && workerThreadCount_ > 0) { + FABRIC_LOG_INFO("Starting {} worker threads", workerThreadCount_.load()); + for (unsigned int i = 0; i < workerThreadCount_; ++i) { + workerThreads_.push_back(std::make_unique(&ResourceHub::workerThreadFunc, this)); + } + } else { + // In test environment, don't start threads automatically + workerThreadCount_ = 0; + FABRIC_LOG_DEBUG("ResourceHub detected test environment - not starting worker threads"); } - } else { - // In test environment, don't start threads automatically - workerThreadCount_ = 0; - FABRIC_LOG_DEBUG("ResourceHub detected test environment - not starting worker threads"); - } } void ResourceHub::clear() { - // Simplified clear implementation using the Copy-Then-Process pattern - constexpr int CLEAR_TIMEOUT_MS = 1000; // 1 second timeout - auto startTime = std::chrono::steady_clock::now(); - - // Timeout checker function - auto isTimedOut = [&startTime, CLEAR_TIMEOUT_MS]() -> bool { - return std::chrono::duration_cast( - std::chrono::steady_clock::now() - startTime) - .count() > CLEAR_TIMEOUT_MS; - }; - - try { - // First, get all resource IDs - std::vector allResourceIds; - try { - allResourceIds = resourceGraph_.getAllNodes(); - } catch (const std::exception &e) { - FABRIC_LOG_ERROR("Failed to get all nodes during clear(): {}", e.what()); - return; - } - - if (allResourceIds.empty()) { - return; // Nothing to clear - } - - // Determine a topological ordering for safe unloading - std::vector orderedIds; + // Simplified clear implementation using the Copy-Then-Process pattern + constexpr int CLEAR_TIMEOUT_MS = 1000; // 1 second timeout + auto startTime = std::chrono::steady_clock::now(); + + // Timeout checker function + auto isTimedOut = [&startTime, CLEAR_TIMEOUT_MS]() -> bool { + return std::chrono::duration_cast(std::chrono::steady_clock::now() - startTime) + .count() > CLEAR_TIMEOUT_MS; + }; + try { - orderedIds = resourceGraph_.topologicalSort(); - if (orderedIds.empty() && !allResourceIds.empty()) { - // Topological sort failed (possibly due to cycles), use the original ID list - FABRIC_LOG_WARN("Topological sort failed during clear(), using unordered approach"); - orderedIds = allResourceIds; - } - } catch (const std::exception &e) { - FABRIC_LOG_ERROR("Error in topological sort during clear(): {}", e.what()); - orderedIds = allResourceIds; // Fall back to unordered - } - - // Process resources in appropriate order - for (auto it = orderedIds.rbegin(); it != orderedIds.rend(); ++it) { - const std::string& id = *it; - - // Check for timeout - if (isTimedOut()) { - FABRIC_LOG_WARN("clear() timed out during resource unloading"); - break; - } - - // First, attempt to unload the resource - try { - // Get the node - auto node = resourceGraph_.getNode(id, 50); - if (!node) { - continue; + // First, get all resource IDs + std::vector allResourceIds; + try { + allResourceIds = resourceGraph_.getAllNodes(); + } catch (const std::exception& e) { + FABRIC_LOG_ERROR("Failed to get all nodes during clear(): {}", e.what()); + return; } - - // Lock the node to access its data - auto nodeLock = node->tryLock( - CoordinatedGraph>::LockIntent::NodeModify, - 50); - - if (!nodeLock || !nodeLock->isLocked()) { - continue; + + if (allResourceIds.empty()) { + return; // Nothing to clear } - - auto resource = nodeLock->getNode()->getDataNoLock(); - if (resource && resource->getState() == ResourceState::Loaded) { - resource->unload(); + + // Determine a topological ordering for safe unloading + std::vector orderedIds; + try { + orderedIds = resourceGraph_.topologicalSort(); + if (orderedIds.empty() && !allResourceIds.empty()) { + // Topological sort failed (possibly due to cycles), use the original ID list + FABRIC_LOG_WARN("Topological sort failed during clear(), using unordered approach"); + orderedIds = allResourceIds; + } + } catch (const std::exception& e) { + FABRIC_LOG_ERROR("Error in topological sort during clear(): {}", e.what()); + orderedIds = allResourceIds; // Fall back to unordered } - - // Release the lock before removing the node - nodeLock->release(); - - // Now remove the node from the graph - resourceGraph_.removeNode(id); - } catch (const std::exception &e) { - FABRIC_LOG_ERROR("Error processing resource {} during clear(): {}", id, e.what()); - } - } - - // Final check if there are still resources left - if (!isTimedOut()) { - try { - auto remainingIds = resourceGraph_.getAllNodes(); - if (!remainingIds.empty()) { - FABRIC_LOG_WARN("Some resources could not be cleared. {} resources remain.", - remainingIds.size()); + + // Process resources in appropriate order + for (auto it = orderedIds.rbegin(); it != orderedIds.rend(); ++it) { + const std::string& id = *it; + + // Check for timeout + if (isTimedOut()) { + FABRIC_LOG_WARN("clear() timed out during resource unloading"); + break; + } + + // First, attempt to unload the resource + try { + // Get the node + auto node = resourceGraph_.getNode(id, 50); + if (!node) { + continue; + } + + // Lock the node to access its data + auto nodeLock = node->tryLock(CoordinatedGraph>::LockIntent::NodeModify, 50); + + if (!nodeLock || !nodeLock->isLocked()) { + continue; + } + + auto resource = nodeLock->getNode()->getDataNoLock(); + if (resource && resource->getState() == ResourceState::Loaded) { + resource->unload(); + } + + // Release the lock before removing the node + nodeLock->release(); + + // Now remove the node from the graph + resourceGraph_.removeNode(id); + } catch (const std::exception& e) { + FABRIC_LOG_ERROR("Error processing resource {} during clear(): {}", id, e.what()); + } + } + + // Final check if there are still resources left + if (!isTimedOut()) { + try { + auto remainingIds = resourceGraph_.getAllNodes(); + if (!remainingIds.empty()) { + FABRIC_LOG_WARN("Some resources could not be cleared. {} resources remain.", remainingIds.size()); + } + } catch (const std::exception& e) { + FABRIC_LOG_ERROR("Error checking remaining resources: {}", e.what()); + } } - } catch (const std::exception &e) { - FABRIC_LOG_ERROR("Error checking remaining resources: {}", e.what()); - } + } catch (const std::exception& e) { + FABRIC_LOG_ERROR("Unexpected exception in clear(): {}", e.what()); } - } catch (const std::exception &e) { - FABRIC_LOG_ERROR("Unexpected exception in clear(): {}", e.what()); - } } void ResourceHub::reset() { - // Disable worker threads - disableWorkerThreadsForTesting(); - - // Clear all resources - clear(); - - // Reset memory budget to default - memoryBudget_ = 1024 * 1024 * 1024; // 1 GB default + // Disable worker threads + disableWorkerThreadsForTesting(); + + // Clear all resources + clear(); + + // Reset memory budget to default + memoryBudget_ = 1024 * 1024 * 1024; // 1 GB default } bool ResourceHub::isEmpty() const { - try { - return resourceGraph_.empty(); - } catch (const std::exception &e) { - FABRIC_LOG_ERROR("Exception in isEmpty(): {}", e.what()); - return false; - } + try { + return resourceGraph_.empty(); + } catch (const std::exception& e) { + FABRIC_LOG_ERROR("Exception in isEmpty(): {}", e.what()); + return false; + } } } // namespace fabric diff --git a/src/core/SceneView.cc b/src/core/SceneView.cc index fc608f4a..95157104 100644 --- a/src/core/SceneView.cc +++ b/src/core/SceneView.cc @@ -30,18 +30,21 @@ void SceneView::render() { dc.viewId = viewId_; // Read pre-computed world transform from CASCADE system - const auto* ltw = entity.get(); + const auto* ltw = entity.try_get(); if (ltw) { dc.transform = ltw->matrix; } else { // Fallback: compose from components if LocalToWorld is missing Transform t; - const auto* pos = entity.get(); - const auto* rot = entity.get(); - const auto* scl = entity.get(); - if (pos) t.setPosition(Vector3(pos->x, pos->y, pos->z)); - if (rot) t.setRotation(Quaternion(rot->x, rot->y, rot->z, rot->w)); - if (scl) t.setScale(Vector3(scl->x, scl->y, scl->z)); + const auto* pos = entity.try_get(); + const auto* rot = entity.try_get(); + const auto* scl = entity.try_get(); + if (pos) + t.setPosition(Vector3(pos->x, pos->y, pos->z)); + if (rot) + t.setRotation(Quaternion(rot->x, rot->y, rot->z, rot->w)); + if (scl) + t.setScale(Vector3(scl->x, scl->y, scl->z)); auto matrix = t.getMatrix(); dc.transform = matrix.elements; } diff --git a/src/core/Simulation.cc b/src/core/Simulation.cc index 7925967c..6b4520df 100644 --- a/src/core/Simulation.cc +++ b/src/core/Simulation.cc @@ -11,9 +11,9 @@ void SimulationHarness::registerRule(const std::string& name, SimRule rule) { } bool SimulationHarness::removeRule(const std::string& name) { - auto it = std::find_if(rules_.begin(), rules_.end(), - [&](const auto& p) { return p.first == name; }); - if (it == rules_.end()) return false; + auto it = std::find_if(rules_.begin(), rules_.end(), [&](const auto& p) { return p.first == name; }); + if (it == rules_.end()) + return false; rules_.erase(it); return true; } @@ -25,12 +25,15 @@ size_t SimulationHarness::ruleCount() const { void SimulationHarness::tick(double dt) { FABRIC_ZONE_SCOPED_N("Simulation::tick"); - if (rules_.empty()) return; + if (rules_.empty()) + return; // Merge active chunks from both fields, deduplicated and sorted std::set> merged; - for (auto& c : density_.grid().activeChunks()) merged.insert(c); - for (auto& c : essence_.grid().activeChunks()) merged.insert(c); + for (auto& c : density_.grid().activeChunks()) + merged.insert(c); + for (auto& c : essence_.grid().activeChunks()) + merged.insert(c); for (auto [cx, cy, cz] : merged) { int baseX = cx * kChunkSize; @@ -51,9 +54,17 @@ void SimulationHarness::tick(double dt) { } } -DensityField& SimulationHarness::density() { return density_; } -const DensityField& SimulationHarness::density() const { return density_; } -EssenceField& SimulationHarness::essence() { return essence_; } -const EssenceField& SimulationHarness::essence() const { return essence_; } +DensityField& SimulationHarness::density() { + return density_; +} +const DensityField& SimulationHarness::density() const { + return density_; +} +EssenceField& SimulationHarness::essence() { + return essence_; +} +const EssenceField& SimulationHarness::essence() const { + return essence_; +} } // namespace fabric diff --git a/src/core/Temporal.cc b/src/core/Temporal.cc index bd95eeab..d9f12aa5 100644 --- a/src/core/Temporal.cc +++ b/src/core/Temporal.cc @@ -15,7 +15,7 @@ double TimeState::getTimestamp() const { std::unordered_map TimeState::diff(const TimeState& other) const { std::unordered_map result; - + // Check entities in this state for (const auto& [id, state] : entityStates_) { auto it = other.entityStates_.find(id); @@ -27,14 +27,14 @@ std::unordered_map TimeState::diff(const TimeState& o result[id] = true; } } - + // Check entities that only exist in the other state for (const auto& [id, state] : other.entityStates_) { if (entityStates_.find(id) == entityStates_.end()) { result[id] = false; } } - + return result; } @@ -76,27 +76,27 @@ void TimeRegion::restoreSnapshot(const TimeState& state) { } // Timeline implementation -Timeline::Timeline() : - currentTime_(0.0), - globalTimeScale_(1.0), - isPaused_(false), - automaticSnapshots_(false), - snapshotInterval_(1.0), - snapshotCounter_(0.0) { +Timeline::Timeline() + : currentTime_(0.0), + globalTimeScale_(1.0), + isPaused_(false), + automaticSnapshots_(false), + snapshotInterval_(1.0), + snapshotCounter_(0.0) { FABRIC_LOG_DEBUG("Timeline initialized"); } void Timeline::update(double deltaTime) { std::lock_guard lock(mutex_); - + if (isPaused_) { return; } - + // Apply global time scale double scaledDelta = deltaTime * globalTimeScale_; currentTime_ += scaledDelta; - + // Accumulate real time and emit a snapshot for each elapsed interval. // Ring buffer caps at 100 entries to bound memory. if (automaticSnapshots_) { @@ -111,7 +111,7 @@ void Timeline::update(double deltaTime) { } } } - + // Update all time regions for (auto& region : regions_) { region->update(scaledDelta); @@ -130,9 +130,7 @@ TimeRegion* Timeline::createRegion(double timeScale) { void Timeline::removeRegion(TimeRegion* region) { std::lock_guard lock(mutex_); auto it = std::find_if(regions_.begin(), regions_.end(), - [region](const std::unique_ptr& r) { - return r.get() == region; - }); + [region](const std::unique_ptr& r) { return r.get() == region; }); if (it != regions_.end()) { regions_.erase(it); @@ -220,4 +218,4 @@ TimeState Timeline::predictFutureState(double secondsAhead) const { return TimeState(currentTime_ + secondsAhead); } -} // namespace fabric \ No newline at end of file +} // namespace fabric diff --git a/src/core/VoxelMesher.cc b/src/core/VoxelMesher.cc index 169cd43d..438a9ffa 100644 --- a/src/core/VoxelMesher.cc +++ b/src/core/VoxelMesher.cc @@ -6,22 +6,13 @@ namespace { // Face directions: +X, -X, +Y, -Y, +Z, -Z constexpr float kNormals[6][3] = { - { 1.0f, 0.0f, 0.0f}, - {-1.0f, 0.0f, 0.0f}, - { 0.0f, 1.0f, 0.0f}, - { 0.0f, -1.0f, 0.0f}, - { 0.0f, 0.0f, 1.0f}, - { 0.0f, 0.0f, -1.0f}, + {1.0f, 0.0f, 0.0f}, {-1.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, + {0.0f, -1.0f, 0.0f}, {0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, -1.0f}, }; // Neighbor offsets matching the face order constexpr int kNeighborOff[6][3] = { - { 1, 0, 0}, - {-1, 0, 0}, - { 0, 1, 0}, - { 0, -1, 0}, - { 0, 0, 1}, - { 0, 0, -1}, + {1, 0, 0}, {-1, 0, 0}, {0, 1, 0}, {0, -1, 0}, {0, 0, 1}, {0, 0, -1}, }; // 4 vertices per face, CCW winding when viewed from outside the cube. @@ -47,18 +38,14 @@ bgfx::VertexLayout VoxelMesher::getVertexLayout() { bgfx::VertexLayout layout; layout.begin() .add(bgfx::Attrib::Position, 3, bgfx::AttribType::Float) - .add(bgfx::Attrib::Normal, 3, bgfx::AttribType::Float) - .add(bgfx::Attrib::Color0, 4, bgfx::AttribType::Float) + .add(bgfx::Attrib::Normal, 3, bgfx::AttribType::Float) + .add(bgfx::Attrib::Color0, 4, bgfx::AttribType::Float) .end(); return layout; } -ChunkMeshData VoxelMesher::meshChunkData( - int cx, int cy, int cz, - const ChunkedGrid& density, - const ChunkedGrid>& essence, - float threshold) -{ +ChunkMeshData VoxelMesher::meshChunkData(int cx, int cy, int cz, const ChunkedGrid& density, + const ChunkedGrid>& essence, float threshold) { ChunkMeshData data; int baseX = cx * kChunkSize; @@ -73,7 +60,8 @@ ChunkMeshData VoxelMesher::meshChunkData( int wz = baseZ + lz; float d = density.get(wx, wy, wz); - if (d <= threshold) continue; + if (d <= threshold) + continue; // Read essence for color auto e = essence.get(wx, wy, wz); @@ -87,10 +75,10 @@ ChunkMeshData VoxelMesher::meshChunkData( } else { // essence = [Order, Chaos, Life, Decay] // Color: R = Chaos, G = Life, B = Order, A = 1 - Decay*0.5 - cr = e.y; // Chaos - cg = e.z; // Life - cb = e.x; // Order - ca = 1.0f - e.w * 0.5f; // Decay + cr = e.y; // Chaos + cg = e.z; // Life + cb = e.x; // Order + ca = 1.0f - e.w * 0.5f; // Decay } for (int face = 0; face < 6; ++face) { @@ -99,7 +87,8 @@ ChunkMeshData VoxelMesher::meshChunkData( int nz = wz + kNeighborOff[face][2]; float nd = density.get(nx, ny, nz); - if (nd > threshold) continue; + if (nd > threshold) + continue; // Emit quad for this face auto base = static_cast(data.vertices.size()); @@ -134,26 +123,20 @@ ChunkMeshData VoxelMesher::meshChunkData( return data; } -ChunkMesh VoxelMesher::meshChunk( - int cx, int cy, int cz, - const ChunkedGrid& density, - const ChunkedGrid>& essence, - float threshold) -{ +ChunkMesh VoxelMesher::meshChunk(int cx, int cy, int cz, const ChunkedGrid& density, + const ChunkedGrid>& essence, float threshold) { auto data = meshChunkData(cx, cy, cz, density, essence, threshold); - if (data.vertices.empty()) return ChunkMesh{}; + if (data.vertices.empty()) + return ChunkMesh{}; ChunkMesh mesh; auto layout = getVertexLayout(); mesh.vbh = bgfx::createVertexBuffer( - bgfx::copy(data.vertices.data(), - static_cast(data.vertices.size() * sizeof(VoxelVertex))), - layout); + bgfx::copy(data.vertices.data(), static_cast(data.vertices.size() * sizeof(VoxelVertex))), layout); mesh.ibh = bgfx::createIndexBuffer( - bgfx::copy(data.indices.data(), - static_cast(data.indices.size() * sizeof(uint32_t))), + bgfx::copy(data.indices.data(), static_cast(data.indices.size() * sizeof(uint32_t))), BGFX_BUFFER_INDEX32); mesh.indexCount = static_cast(data.indices.size()); diff --git a/src/parser/ArgumentParser.cc b/src/parser/ArgumentParser.cc index 474cf2ff..fcfa2a23 100644 --- a/src/parser/ArgumentParser.cc +++ b/src/parser/ArgumentParser.cc @@ -1,7 +1,7 @@ #include "fabric/parser/ArgumentParser.hh" #include "fabric/core/Constants.g.hh" -#include "fabric/utils/ErrorHandling.hh" #include "fabric/core/Log.hh" +#include "fabric/utils/ErrorHandling.hh" #include #include @@ -9,163 +9,157 @@ namespace fabric { // Constructor is defaulted in the header, no need to define here // Add a new command line argument -void ArgumentParser::addArgument(const std::string &name, - const std::string &description, - bool required) { - // Store the argument definition - availableArgs[name] = {TokenType::LiteralString, !required}; - argumentDescriptions[name] = description; - FABRIC_LOG_DEBUG("Added argument: {} ({})", name, - required ? "required" : "optional"); +void ArgumentParser::addArgument(const std::string& name, const std::string& description, bool required) { + // Store the argument definition + availableArgs[name] = {TokenType::LiteralString, !required}; + argumentDescriptions[name] = description; + FABRIC_LOG_DEBUG("Added argument: {} ({})", name, required ? "required" : "optional"); } // Check if an argument exists in parsed arguments -bool ArgumentParser::hasArgument(const std::string &name) const { - return arguments.find(name) != arguments.end(); +bool ArgumentParser::hasArgument(const std::string& name) const { + return arguments.find(name) != arguments.end(); } // Get argument value -const OptionalToken -ArgumentParser::getArgument(const std::string &name) const { - auto it = arguments.find(name); - if (it != arguments.end()) { - return it->second; - } - return std::nullopt; +const OptionalToken ArgumentParser::getArgument(const std::string& name) const { + auto it = arguments.find(name); + if (it != arguments.end()) { + return it->second; + } + return std::nullopt; } -const TokenMap &ArgumentParser::getArguments() const { - return arguments; +const TokenMap& ArgumentParser::getArguments() const { + return arguments; } // Parse arguments using SyntaxTree -void ArgumentParser::parse(int argc, char *argv[]) { - try { - // Skip the program name (argv[0]) - for (int i = 1; i < argc; i++) { - std::string arg = argv[i]; - - // Check if it's an option (starts with --) - if (arg.length() >= 2 && arg.substr(0, 2) == "--") { - std::string optionName = arg; - - // Check if the next argument is a value (not an option) - if (i + 1 < argc && argv[i + 1][0] != '-') { - // For simplicity in testing, always use LiteralString for values - // and we'll trust the proper token type conversion elsewhere - Variant value = argv[i + 1]; - arguments[optionName] = Token(TokenType::LiteralString, value); - i++; // Skip the value in the next iteration - } else { - // Flag without value, set to true - Variant value = true; - arguments[optionName] = Token(TokenType::CLIFlag, value); +void ArgumentParser::parse(int argc, char* argv[]) { + try { + // Skip the program name (argv[0]) + for (int i = 1; i < argc; i++) { + std::string arg = argv[i]; + + // Check if it's an option (starts with --) + if (arg.length() >= 2 && arg.substr(0, 2) == "--") { + std::string optionName = arg; + + // Check if the next argument is a value (not an option) + if (i + 1 < argc && argv[i + 1][0] != '-') { + // For simplicity in testing, always use LiteralString for values + // and we'll trust the proper token type conversion elsewhere + Variant value = argv[i + 1]; + arguments[optionName] = Token(TokenType::LiteralString, value); + i++; // Skip the value in the next iteration + } else { + // Flag without value, set to true + Variant value = true; + arguments[optionName] = Token(TokenType::CLIFlag, value); + } + } else { + // Handle positional arguments if needed + // For now, store them with a special prefix + std::string posName = "pos" + std::to_string(i); + Variant value = arg; + arguments[posName] = Token(TokenType::LiteralString, value); + } } - } else { - // Handle positional arguments if needed - // For now, store them with a special prefix - std::string posName = "pos" + std::to_string(i); - Variant value = arg; - arguments[posName] = Token(TokenType::LiteralString, value); - } - } - // After parsing, validate required options - validateArgs(availableArgs); - if (!valid) - return; - } catch (const std::exception &e) { - FABRIC_LOG_ERROR("Error parsing arguments: {}", e.what()); - } + // After parsing, validate required options + validateArgs(availableArgs); + if (!valid) + return; + } catch (const std::exception& e) { + FABRIC_LOG_ERROR("Error parsing arguments: {}", e.what()); + } } -void ArgumentParser::parse(const std::string &args) { - try { - std::istringstream stream(args); - std::string arg; - std::vector argv; +void ArgumentParser::parse(const std::string& args) { + try { + std::istringstream stream(args); + std::string arg; + std::vector argv; - // Split the string into tokens - while (stream >> arg) { - argv.push_back(arg); - } + // Split the string into tokens + while (stream >> arg) { + argv.push_back(arg); + } - // Process the tokens similar to the other overload - for (size_t i = 0; i < argv.size(); i++) { - // Check if it's an option (starts with --) - if (argv[i].length() >= 2 && argv[i].substr(0, 2) == "--") { - std::string optionName = argv[i]; - - // Check if the next argument is a value (not an option) - if (i + 1 < argv.size() && argv[i + 1][0] != '-') { - // For simplicity in testing, always use LiteralString for values - Variant value = argv[i + 1]; - arguments[optionName] = Token(TokenType::LiteralString, value); - i++; // Skip the value in the next iteration - } else { - // Flag without value, set to true - Variant value = true; - arguments[optionName] = Token(TokenType::CLIFlag, value); + // Process the tokens similar to the other overload + for (size_t i = 0; i < argv.size(); i++) { + // Check if it's an option (starts with --) + if (argv[i].length() >= 2 && argv[i].substr(0, 2) == "--") { + std::string optionName = argv[i]; + + // Check if the next argument is a value (not an option) + if (i + 1 < argv.size() && argv[i + 1][0] != '-') { + // For simplicity in testing, always use LiteralString for values + Variant value = argv[i + 1]; + arguments[optionName] = Token(TokenType::LiteralString, value); + i++; // Skip the value in the next iteration + } else { + // Flag without value, set to true + Variant value = true; + arguments[optionName] = Token(TokenType::CLIFlag, value); + } + } else { + // Handle positional arguments if needed + std::string posName = "pos" + std::to_string(i); + Variant value = argv[i]; + arguments[posName] = Token(TokenType::LiteralString, value); + } } - } else { - // Handle positional arguments if needed - std::string posName = "pos" + std::to_string(i); - Variant value = argv[i]; - arguments[posName] = Token(TokenType::LiteralString, value); - } - } - // After parsing, validate required options - validateArgs(availableArgs); - if (!valid) - return; - } catch (const std::exception &e) { - FABRIC_LOG_ERROR("Error parsing arguments: {}", e.what()); - } + // After parsing, validate required options + validateArgs(availableArgs); + if (!valid) + return; + } catch (const std::exception& e) { + FABRIC_LOG_ERROR("Error parsing arguments: {}", e.what()); + } } -bool ArgumentParser::validateArgs(const TokenTypeOptionsMap &options) { - bool valid = true; - std::vector missingArgs; +bool ArgumentParser::validateArgs(const TokenTypeOptionsMap& options) { + bool valid = true; + std::vector missingArgs; - for (const auto &arg_pair : options) { - const std::string &name = arg_pair.first; - const bool optional = arg_pair.second.second; + for (const auto& arg_pair : options) { + const std::string& name = arg_pair.first; + const bool optional = arg_pair.second.second; - if (!optional && arguments.find(name) == arguments.end()) { - valid = false; - missingArgs.push_back(name); - } - } - - if (!valid) { - std::string errorMsg = "Missing required arguments: "; - for (size_t i = 0; i < missingArgs.size(); ++i) { - errorMsg += missingArgs[i]; - if (i < missingArgs.size() - 1) { - errorMsg += ", "; - } + if (!optional && arguments.find(name) == arguments.end()) { + valid = false; + missingArgs.push_back(name); + } } - this->errorMsg = errorMsg; - FABRIC_LOG_ERROR("{}", errorMsg); - } + if (!valid) { + std::string errorMsg = "Missing required arguments: "; + for (size_t i = 0; i < missingArgs.size(); ++i) { + errorMsg += missingArgs[i]; + if (i < missingArgs.size() - 1) { + errorMsg += ", "; + } + } + + this->errorMsg = errorMsg; + FABRIC_LOG_ERROR("{}", errorMsg); + } - this->valid = valid; - return valid; + this->valid = valid; + return valid; } // Builder pattern implementation -ArgumentParserBuilder &ArgumentParserBuilder::addOption(const std::string &name, - TokenType type, - bool optional) { - options[name] = std::make_pair(type, optional); - return *this; +ArgumentParserBuilder& ArgumentParserBuilder::addOption(const std::string& name, TokenType type, bool optional) { + options[name] = std::make_pair(type, optional); + return *this; } ArgumentParser ArgumentParserBuilder::build() const { - ArgumentParser parser; - parser.availableArgs = options; - return parser; + ArgumentParser parser; + parser.availableArgs = options; + return parser; } } // namespace fabric diff --git a/src/parser/SyntaxTree.cc b/src/parser/SyntaxTree.cc index a4ed6612..60b42fdc 100644 --- a/src/parser/SyntaxTree.cc +++ b/src/parser/SyntaxTree.cc @@ -1,467 +1,460 @@ #include "fabric/parser/SyntaxTree.hh" -#include "fabric/utils/ErrorHandling.hh" #include "fabric/core/Log.hh" +#include "fabric/utils/ErrorHandling.hh" namespace fabric { -ASTNode::ASTNode(const Token &token) : token(token) {} +ASTNode::ASTNode(const Token& token) : token(token) {} void ASTNode::addChild(std::shared_ptr child) { - children.push_back(child); + children.push_back(child); +} + +const std::vector>& ASTNode::getChildren() const { + return children; } -const std::vector> &ASTNode::getChildren() const { - return children; +const Token& ASTNode::getToken() const { + return token; } -const Token &ASTNode::getToken() const { return token; } - -TokenType determineTokenType(const std::string &token) { - // CLI - if (token.size() > 2 && token.substr(0, 2) == "--") { - // Check if it's a flag with a value (--option=value) - size_t equalsPos = token.find('='); - if (equalsPos != std::string::npos) - return TokenType::CLIOption; - - // Otherwise it's a simple flag (--flag) - return TokenType::CLIFlag; - } - if (token.size() > 1 && token[0] == '-' && token[1] != '-') - return TokenType::CLIFlag; // Short options (-f) - - // Control Flow - if (token == "if") - return TokenType::KeywordIf; - if (token == "else") - return TokenType::KeywordElse; - if (token == "for") - return TokenType::KeywordFor; - if (token == "while") - return TokenType::KeywordWhile; - if (token == "return") - return TokenType::KeywordReturn; - if (token == "goto") - return TokenType::KeywordGoto; - if (token == "break") - return TokenType::KeywordBreak; - if (token == "continue") - return TokenType::KeywordContinue; - if (token == "switch") - return TokenType::KeywordSwitch; - if (token == "case") - return TokenType::KeywordCase; - if (token == "default") - return TokenType::KeywordDefault; - if (token == "defer") - return TokenType::KeywordDefer; - - // Error Handling - if (token == "try") - return TokenType::KeywordTry; - if (token == "catch") - return TokenType::KeywordCatch; - if (token == "throw") - return TokenType::KeywordThrow; - if (token == "finally") - return TokenType::KeywordFinally; - if (token == "raise") - return TokenType::KeywordRaise; - if (token == "assert") - return TokenType::KeywordAssert; - - // Data Types - if (token == "func" || token == "def" || token == "fn" || token == "function") - return TokenType::KeywordFunction; - if (token == "struct") - return TokenType::KeywordStruct; - if (token == "enum") - return TokenType::KeywordEnum; - if (token == "array") - return TokenType::KeywordArray; - if (token == "map" || token == "dict") - return TokenType::KeywordMap; - if (token == "set") - return TokenType::KeywordSet; - if (token == "tuple") - return TokenType::KeywordTuple; - if (token == "generic" || token == "template") - return TokenType::KeywordGeneric; - if (token == "where") - return TokenType::KeywordWhere; - - // Object-Oriented Inheritance - if (token == "class") - return TokenType::KeywordClass; - if (token == "interface") - return TokenType::KeywordInterface; - if (token == "implements") - return TokenType::KeywordImplements; - if (token == "extends") - return TokenType::KeywordExtends; - if (token == "self") - return TokenType::KeywordSelf; - if (token == "super") - return TokenType::KeywordSuper; - if (token == "override") - return TokenType::KeywordOverride; - if (token == "abstract") - return TokenType::KeywordAbstract; - if (token == "virtual") - return TokenType::KeywordVirtual; - if (token == "delegate") - return TokenType::KeywordDelegate; - if (token == "event") - return TokenType::KeywordEvent; - - // Modules - if (token == "import" || token == "include" || token == "use") - return TokenType::KeywordImport; - if (token == "package" || token == "module" || token == "namespace") - return TokenType::KeywordPackage; - if (token == "export") - return TokenType::KeywordExport; - if (token == "from") - return TokenType::KeywordFrom; - - // Declarations - if (token == "const") - return TokenType::KeywordConst; - if (token == "let") - return TokenType::KeywordLet; - if (token == "var") - return TokenType::KeywordVar; - if (token == "type") - return TokenType::KeywordType; - if (token == "mut") - return TokenType::KeywordMut; - if (token == "unsafe") - return TokenType::KeywordUnsafe; - if (token == "static") - return TokenType::KeywordStatic; - - // Memory Management - if (token == "new") - return TokenType::KeywordNew; - if (token == "delete") - return TokenType::KeywordDelete; - if (token == "alloc") - return TokenType::KeywordAlloc; - if (token == "free") - return TokenType::KeywordFree; - if (token == "move") - return TokenType::KeywordMove; - if (token == "borrow") - return TokenType::KeywordBorrow; - - // Access Modifiers - if (token == "pub" || token == "public") - return TokenType::KeywordPublic; - if (token == "priv" || token == "private") - return TokenType::KeywordPrivate; - if (token == "prot" || token == "protected") - return TokenType::KeywordProtected; - if (token == "int" || token == "internal") - return TokenType::KeywordInternal; - if (token == "final") - return TokenType::KeywordFinal; - - // Boolean Operators - if (token == "as") - return TokenType::KeywordAs; - if (token == "is") - return TokenType::KeywordIs; - if (token == "in") - return TokenType::KeywordIn; - if (token == "not") - return TokenType::KeywordNot; - if (token == "and") - return TokenType::KeywordAnd; - if (token == "or") - return TokenType::KeywordOr; - - // Functional Programming - if (token == "lambda" || token == "=>") - return TokenType::KeywordLambda; - if (token == "closure") - return TokenType::KeywordClosure; - if (token == "curry") - return TokenType::KeywordCurry; - if (token == "pipe" || token == "|>") - return TokenType::KeywordPipe; - if (token == "compose") - return TokenType::KeywordCompose; - - // Concurrency - if (token == "thread") - return TokenType::KeywordThread; - if (token == "atomic") - return TokenType::KeywordAtomic; - if (token == "sync") - return TokenType::KeywordSync; - if (token == "lock") - return TokenType::KeywordLock; - if (token == "mutex") - return TokenType::KeywordMutex; - - // Async - if (token == "yield") - return TokenType::KeywordYield; - if (token == "async") - return TokenType::KeywordAsync; - if (token == "await") - return TokenType::KeywordAwait; - - // Operators - if (token == "+") - return TokenType::OperatorPlus; - if (token == "-") - return TokenType::OperatorMinus; - if (token == "*") - return TokenType::OperatorMultiply; - if (token == "/") - return TokenType::OperatorDivide; - if (token == "%") - return TokenType::OperatorModulo; - if (token == "=") - return TokenType::OperatorAssign; - if (token == "==") - return TokenType::OperatorEqual; - if (token == "!=" || token == "<>") - return TokenType::OperatorNotEqual; - if (token == "<") - return TokenType::OperatorLessThan; - if (token == ">") - return TokenType::OperatorGreaterThan; - if (token == "<=") - return TokenType::OperatorLessEqual; - if (token == ">=") - return TokenType::OperatorGreaterEqual; - if (token == "**") - return TokenType::OperatorPower; - if (token == "&") - return TokenType::OperatorBitwiseAnd; - if (token == "|") - return TokenType::OperatorBitwiseOr; - if (token == "^") - return TokenType::OperatorBitwiseXor; - if (token == "~") - return TokenType::OperatorBitwiseNot; - if (token == "<<") - return TokenType::OperatorShiftLeft; - if (token == ">>") - return TokenType::OperatorShiftRight; - if (token == "+=") - return TokenType::OperatorAssignAdd; - if (token == "-=") - return TokenType::OperatorAssignSubtract; - if (token == "*=") - return TokenType::OperatorAssignMultiply; - if (token == "/=") - return TokenType::OperatorAssignDivide; - if (token == "%=") - return TokenType::OperatorAssignModulo; - if (token == "&=") - return TokenType::OperatorAssignBitwiseAnd; - if (token == "|=") - return TokenType::OperatorAssignBitwiseOr; - if (token == "^=") - return TokenType::OperatorAssignBitwiseXor; - if (token == "~=") - return TokenType::OperatorAssignBitwiseNot; - if (token == "<<=") - return TokenType::OperatorAssignShiftLeft; - if (token == ">>=") - return TokenType::OperatorAssignShiftRight; - if (token == "**=") - return TokenType::OperatorAssignPower; - if (token == "++") - return TokenType::OperatorIncrement; - if (token == "--") - return TokenType::OperatorDecrement; - if (token == "??") - return TokenType::OperatorNullCoalesce; - if (token == "?.") - return TokenType::OperatorOptionalChaining; - if (token == "...") - return TokenType::OperatorSpread; - if (token == "..=") - return TokenType::OperatorRangeInclusive; - if (token == "..") - return TokenType::OperatorRangeExclusive; - if (token == "|>") - return TokenType::OperatorPipeline; - - // Delimiters - if (token == ";") - return TokenType::DelimiterSemicolon; - if (token == ",") - return TokenType::DelimiterComma; - if (token == ".") - return TokenType::DelimiterDot; - if (token == ":") - return TokenType::DelimiterColon; - if (token == "(") - return TokenType::DelimiterOpenParen; - if (token == ")") - return TokenType::DelimiterCloseParen; - if (token == "{") - return TokenType::DelimiterOpenBrace; - if (token == "}") - return TokenType::DelimiterCloseBrace; - if (token == "[") - return TokenType::DelimiterOpenBracket; - if (token == "]") - return TokenType::DelimiterCloseBracket; - if (token == "::") - return TokenType::DelimiterDoubleColon; - if (token == "->") - return TokenType::DelimiterArrow; - if (token == "=>") - return TokenType::DelimiterFatArrow; - if (token == "`") - return TokenType::DelimiterBacktick; - - // Literals - if (token == "null" || token == "nil" || token == "None") - return TokenType::LiteralNull; - - // Check for numeric literals - if (token.find("0b") == 0 && - token.find_first_not_of("01", 2) == std::string::npos) - return TokenType::LiteralBinary; - if (token.find("0x") == 0 && - token.find_first_not_of("0123456789abcdefABCDEF", 2) == std::string::npos) - return TokenType::LiteralHex; - if (token.find("0o") == 0 && - token.find_first_not_of("01234567", 2) == std::string::npos) - return TokenType::LiteralOctal; - if (token.find_first_not_of("0123456789") == std::string::npos) - return TokenType::LiteralNumber; - if (token.find_first_not_of("0123456789.eE+-") == std::string::npos && - token.find_first_of(".eE") != std::string::npos) - return TokenType::LiteralFloat; - if (token.find_first_not_of("0123456789") == token.length() - 1 && - token.back() == 'n') - return TokenType::LiteralBigInt; - - // String and character literals - if (token.size() >= 2 && token.front() == '"' && token.back() == '"') - return TokenType::LiteralString; - if (token.size() >= 2 && token.front() == '\'' && token.back() == '\'') - return TokenType::LiteralChar; - if (token.size() >= 2 && token.front() == '`' && token.back() == '`') - return TokenType::LiteralTemplate; - if (token.size() >= 2 && - ((token.front() == '/' && token.back() == '/' && token.size() > 2) || - (token.length() > 2 && token[0] == '/' && - token[token.length() - 1] == '/' && - token.find_first_of("*/+") == std::string::npos))) - return TokenType::LiteralRegex; - - // Boolean literals - if (token == "true" || token == "false") - return TokenType::LiteralBoolean; - - // Date literals (simple ISO format check) - if (token.size() == 10 && token[4] == '-' && token[7] == '-' && - isdigit(token[0]) && isdigit(token[1]) && isdigit(token[2]) && - isdigit(token[3]) && isdigit(token[5]) && isdigit(token[6]) && - isdigit(token[8]) && isdigit(token[9])) - return TokenType::LiteralDate; - - // Preprocessor directives - if (token == "#include") - return TokenType::PreprocessorInclude; - if (token == "#define") - return TokenType::PreprocessorDefine; - if (token == "#if") - return TokenType::PreprocessorIf; - if (token == "#else") - return TokenType::PreprocessorElse; - if (token == "#endif") - return TokenType::PreprocessorEndif; - - // Meta-programming - if (token == "quote") - return TokenType::MetaQuote; - if (token == "unquote") - return TokenType::MetaUnquote; - if (token == "splice") - return TokenType::MetaSplice; - if (token == "macro") - return TokenType::MetaMacro; - - // Comments - if (token.size() >= 2 && token.substr(0, 2) == "//") - return TokenType::CommentLine; - if (token.size() >= 1 && token[0] == '#') - return TokenType::CommentLine; - if (token.size() >= 4 && token.substr(0, 2) == "/*" && - token.substr(token.size() - 2) == "*/") - return TokenType::CommentBlock; - - // Whitespace - if (token == "\n") - return TokenType::Newline; - if (token == "\t") - return TokenType::Tab; - if (token == "\r") - return TokenType::CarriageReturn; - if (token == " ") - return TokenType::Space; - if (token.find_first_not_of(" \t\r\n") == std::string::npos) - return TokenType::Whitespace; - - // Default to identifier - return TokenType::Identifier; +TokenType determineTokenType(const std::string& token) { + // CLI + if (token.size() > 2 && token.substr(0, 2) == "--") { + // Check if it's a flag with a value (--option=value) + size_t equalsPos = token.find('='); + if (equalsPos != std::string::npos) + return TokenType::CLIOption; + + // Otherwise it's a simple flag (--flag) + return TokenType::CLIFlag; + } + if (token.size() > 1 && token[0] == '-' && token[1] != '-') + return TokenType::CLIFlag; // Short options (-f) + + // Control Flow + if (token == "if") + return TokenType::KeywordIf; + if (token == "else") + return TokenType::KeywordElse; + if (token == "for") + return TokenType::KeywordFor; + if (token == "while") + return TokenType::KeywordWhile; + if (token == "return") + return TokenType::KeywordReturn; + if (token == "goto") + return TokenType::KeywordGoto; + if (token == "break") + return TokenType::KeywordBreak; + if (token == "continue") + return TokenType::KeywordContinue; + if (token == "switch") + return TokenType::KeywordSwitch; + if (token == "case") + return TokenType::KeywordCase; + if (token == "default") + return TokenType::KeywordDefault; + if (token == "defer") + return TokenType::KeywordDefer; + + // Error Handling + if (token == "try") + return TokenType::KeywordTry; + if (token == "catch") + return TokenType::KeywordCatch; + if (token == "throw") + return TokenType::KeywordThrow; + if (token == "finally") + return TokenType::KeywordFinally; + if (token == "raise") + return TokenType::KeywordRaise; + if (token == "assert") + return TokenType::KeywordAssert; + + // Data Types + if (token == "func" || token == "def" || token == "fn" || token == "function") + return TokenType::KeywordFunction; + if (token == "struct") + return TokenType::KeywordStruct; + if (token == "enum") + return TokenType::KeywordEnum; + if (token == "array") + return TokenType::KeywordArray; + if (token == "map" || token == "dict") + return TokenType::KeywordMap; + if (token == "set") + return TokenType::KeywordSet; + if (token == "tuple") + return TokenType::KeywordTuple; + if (token == "generic" || token == "template") + return TokenType::KeywordGeneric; + if (token == "where") + return TokenType::KeywordWhere; + + // Object-Oriented Inheritance + if (token == "class") + return TokenType::KeywordClass; + if (token == "interface") + return TokenType::KeywordInterface; + if (token == "implements") + return TokenType::KeywordImplements; + if (token == "extends") + return TokenType::KeywordExtends; + if (token == "self") + return TokenType::KeywordSelf; + if (token == "super") + return TokenType::KeywordSuper; + if (token == "override") + return TokenType::KeywordOverride; + if (token == "abstract") + return TokenType::KeywordAbstract; + if (token == "virtual") + return TokenType::KeywordVirtual; + if (token == "delegate") + return TokenType::KeywordDelegate; + if (token == "event") + return TokenType::KeywordEvent; + + // Modules + if (token == "import" || token == "include" || token == "use") + return TokenType::KeywordImport; + if (token == "package" || token == "module" || token == "namespace") + return TokenType::KeywordPackage; + if (token == "export") + return TokenType::KeywordExport; + if (token == "from") + return TokenType::KeywordFrom; + + // Declarations + if (token == "const") + return TokenType::KeywordConst; + if (token == "let") + return TokenType::KeywordLet; + if (token == "var") + return TokenType::KeywordVar; + if (token == "type") + return TokenType::KeywordType; + if (token == "mut") + return TokenType::KeywordMut; + if (token == "unsafe") + return TokenType::KeywordUnsafe; + if (token == "static") + return TokenType::KeywordStatic; + + // Memory Management + if (token == "new") + return TokenType::KeywordNew; + if (token == "delete") + return TokenType::KeywordDelete; + if (token == "alloc") + return TokenType::KeywordAlloc; + if (token == "free") + return TokenType::KeywordFree; + if (token == "move") + return TokenType::KeywordMove; + if (token == "borrow") + return TokenType::KeywordBorrow; + + // Access Modifiers + if (token == "pub" || token == "public") + return TokenType::KeywordPublic; + if (token == "priv" || token == "private") + return TokenType::KeywordPrivate; + if (token == "prot" || token == "protected") + return TokenType::KeywordProtected; + if (token == "int" || token == "internal") + return TokenType::KeywordInternal; + if (token == "final") + return TokenType::KeywordFinal; + + // Boolean Operators + if (token == "as") + return TokenType::KeywordAs; + if (token == "is") + return TokenType::KeywordIs; + if (token == "in") + return TokenType::KeywordIn; + if (token == "not") + return TokenType::KeywordNot; + if (token == "and") + return TokenType::KeywordAnd; + if (token == "or") + return TokenType::KeywordOr; + + // Functional Programming + if (token == "lambda" || token == "=>") + return TokenType::KeywordLambda; + if (token == "closure") + return TokenType::KeywordClosure; + if (token == "curry") + return TokenType::KeywordCurry; + if (token == "pipe" || token == "|>") + return TokenType::KeywordPipe; + if (token == "compose") + return TokenType::KeywordCompose; + + // Concurrency + if (token == "thread") + return TokenType::KeywordThread; + if (token == "atomic") + return TokenType::KeywordAtomic; + if (token == "sync") + return TokenType::KeywordSync; + if (token == "lock") + return TokenType::KeywordLock; + if (token == "mutex") + return TokenType::KeywordMutex; + + // Async + if (token == "yield") + return TokenType::KeywordYield; + if (token == "async") + return TokenType::KeywordAsync; + if (token == "await") + return TokenType::KeywordAwait; + + // Operators + if (token == "+") + return TokenType::OperatorPlus; + if (token == "-") + return TokenType::OperatorMinus; + if (token == "*") + return TokenType::OperatorMultiply; + if (token == "/") + return TokenType::OperatorDivide; + if (token == "%") + return TokenType::OperatorModulo; + if (token == "=") + return TokenType::OperatorAssign; + if (token == "==") + return TokenType::OperatorEqual; + if (token == "!=" || token == "<>") + return TokenType::OperatorNotEqual; + if (token == "<") + return TokenType::OperatorLessThan; + if (token == ">") + return TokenType::OperatorGreaterThan; + if (token == "<=") + return TokenType::OperatorLessEqual; + if (token == ">=") + return TokenType::OperatorGreaterEqual; + if (token == "**") + return TokenType::OperatorPower; + if (token == "&") + return TokenType::OperatorBitwiseAnd; + if (token == "|") + return TokenType::OperatorBitwiseOr; + if (token == "^") + return TokenType::OperatorBitwiseXor; + if (token == "~") + return TokenType::OperatorBitwiseNot; + if (token == "<<") + return TokenType::OperatorShiftLeft; + if (token == ">>") + return TokenType::OperatorShiftRight; + if (token == "+=") + return TokenType::OperatorAssignAdd; + if (token == "-=") + return TokenType::OperatorAssignSubtract; + if (token == "*=") + return TokenType::OperatorAssignMultiply; + if (token == "/=") + return TokenType::OperatorAssignDivide; + if (token == "%=") + return TokenType::OperatorAssignModulo; + if (token == "&=") + return TokenType::OperatorAssignBitwiseAnd; + if (token == "|=") + return TokenType::OperatorAssignBitwiseOr; + if (token == "^=") + return TokenType::OperatorAssignBitwiseXor; + if (token == "~=") + return TokenType::OperatorAssignBitwiseNot; + if (token == "<<=") + return TokenType::OperatorAssignShiftLeft; + if (token == ">>=") + return TokenType::OperatorAssignShiftRight; + if (token == "**=") + return TokenType::OperatorAssignPower; + if (token == "++") + return TokenType::OperatorIncrement; + if (token == "--") + return TokenType::OperatorDecrement; + if (token == "??") + return TokenType::OperatorNullCoalesce; + if (token == "?.") + return TokenType::OperatorOptionalChaining; + if (token == "...") + return TokenType::OperatorSpread; + if (token == "..=") + return TokenType::OperatorRangeInclusive; + if (token == "..") + return TokenType::OperatorRangeExclusive; + if (token == "|>") + return TokenType::OperatorPipeline; + + // Delimiters + if (token == ";") + return TokenType::DelimiterSemicolon; + if (token == ",") + return TokenType::DelimiterComma; + if (token == ".") + return TokenType::DelimiterDot; + if (token == ":") + return TokenType::DelimiterColon; + if (token == "(") + return TokenType::DelimiterOpenParen; + if (token == ")") + return TokenType::DelimiterCloseParen; + if (token == "{") + return TokenType::DelimiterOpenBrace; + if (token == "}") + return TokenType::DelimiterCloseBrace; + if (token == "[") + return TokenType::DelimiterOpenBracket; + if (token == "]") + return TokenType::DelimiterCloseBracket; + if (token == "::") + return TokenType::DelimiterDoubleColon; + if (token == "->") + return TokenType::DelimiterArrow; + if (token == "=>") + return TokenType::DelimiterFatArrow; + if (token == "`") + return TokenType::DelimiterBacktick; + + // Literals + if (token == "null" || token == "nil" || token == "None") + return TokenType::LiteralNull; + + // Check for numeric literals + if (token.starts_with("0b") && token.find_first_not_of("01", 2) == std::string::npos) + return TokenType::LiteralBinary; + if (token.starts_with("0x") && token.find_first_not_of("0123456789abcdefABCDEF", 2) == std::string::npos) + return TokenType::LiteralHex; + if (token.starts_with("0o") && token.find_first_not_of("01234567", 2) == std::string::npos) + return TokenType::LiteralOctal; + if (token.find_first_not_of("0123456789") == std::string::npos) + return TokenType::LiteralNumber; + if (token.find_first_not_of("0123456789.eE+-") == std::string::npos && + token.find_first_of(".eE") != std::string::npos) + return TokenType::LiteralFloat; + if (token.find_first_not_of("0123456789") == token.length() - 1 && token.back() == 'n') + return TokenType::LiteralBigInt; + + // String and character literals + if (token.size() >= 2 && token.front() == '"' && token.back() == '"') + return TokenType::LiteralString; + if (token.size() >= 2 && token.front() == '\'' && token.back() == '\'') + return TokenType::LiteralChar; + if (token.size() >= 2 && token.front() == '`' && token.back() == '`') + return TokenType::LiteralTemplate; + if (token.size() >= 2 && ((token.front() == '/' && token.back() == '/' && token.size() > 2) || + (token.length() > 2 && token[0] == '/' && token[token.length() - 1] == '/' && + token.find_first_of("*/+") == std::string::npos))) + return TokenType::LiteralRegex; + + // Boolean literals + if (token == "true" || token == "false") + return TokenType::LiteralBoolean; + + // Date literals (simple ISO format check) + if (token.size() == 10 && token[4] == '-' && token[7] == '-' && isdigit(token[0]) && isdigit(token[1]) && + isdigit(token[2]) && isdigit(token[3]) && isdigit(token[5]) && isdigit(token[6]) && isdigit(token[8]) && + isdigit(token[9])) + return TokenType::LiteralDate; + + // Preprocessor directives + if (token == "#include") + return TokenType::PreprocessorInclude; + if (token == "#define") + return TokenType::PreprocessorDefine; + if (token == "#if") + return TokenType::PreprocessorIf; + if (token == "#else") + return TokenType::PreprocessorElse; + if (token == "#endif") + return TokenType::PreprocessorEndif; + + // Meta-programming + if (token == "quote") + return TokenType::MetaQuote; + if (token == "unquote") + return TokenType::MetaUnquote; + if (token == "splice") + return TokenType::MetaSplice; + if (token == "macro") + return TokenType::MetaMacro; + + // Comments + if (token.size() >= 2 && token.substr(0, 2) == "//") + return TokenType::CommentLine; + if (token.size() >= 1 && token[0] == '#') + return TokenType::CommentLine; + if (token.size() >= 4 && token.substr(0, 2) == "/*" && token.substr(token.size() - 2) == "*/") + return TokenType::CommentBlock; + + // Whitespace + if (token == "\n") + return TokenType::Newline; + if (token == "\t") + return TokenType::Tab; + if (token == "\r") + return TokenType::CarriageReturn; + if (token == " ") + return TokenType::Space; + if (token.find_first_not_of(" \t\r\n") == std::string::npos) + return TokenType::Whitespace; + + // Default to identifier + return TokenType::Identifier; } -Variant parseValue(const std::string &token, TokenType type) { - try { - size_t equalsPos; - - switch (type) { - case TokenType::CLIFlag: - return true; - case TokenType::CLIOption: - equalsPos = token.find('='); - if (equalsPos != std::string::npos && equalsPos < token.length() - 1) - return token.substr(equalsPos + 1); - return true; - case TokenType::LiteralNumber: - return std::stoi(token); - case TokenType::LiteralFloat: - return std::stod(token); - case TokenType::LiteralString: - return std::string(token.substr(1, token.length() - 2)); - case TokenType::LiteralBoolean: - return token == "true"; - case TokenType::LiteralChar: - return std::string(token.substr(1, token.length() - 2)); - case TokenType::LiteralBinary: - return std::stoi(token.substr(2), nullptr, 2); - case TokenType::LiteralHex: - return std::stoi(token.substr(2), nullptr, 16); - case TokenType::LiteralOctal: - return std::stoi(token.substr(2), nullptr, 8); - case TokenType::LiteralNull: - return nullptr; - case TokenType::LiteralTemplate: - return std::string(token.substr(1, token.length() - 2)); - case TokenType::LiteralRegex: - return std::string(token.substr(1, token.length() - 2)); - case TokenType::LiteralDate: - return token; // Store as string, could be parsed later - case TokenType::LiteralBigInt: - return token.substr(0, token.length() - - 1); // Remove 'n' suffix and store as string - default: - return token; +Variant parseValue(const std::string& token, TokenType type) { + try { + size_t equalsPos; + + switch (type) { + case TokenType::CLIFlag: + return true; + case TokenType::CLIOption: + equalsPos = token.find('='); + if (equalsPos != std::string::npos && equalsPos < token.length() - 1) + return token.substr(equalsPos + 1); + return true; + case TokenType::LiteralNumber: + return std::stoi(token); + case TokenType::LiteralFloat: + return std::stod(token); + case TokenType::LiteralString: + return std::string(token.substr(1, token.length() - 2)); + case TokenType::LiteralBoolean: + return token == "true"; + case TokenType::LiteralChar: + return std::string(token.substr(1, token.length() - 2)); + case TokenType::LiteralBinary: + return std::stoi(token.substr(2), nullptr, 2); + case TokenType::LiteralHex: + return std::stoi(token.substr(2), nullptr, 16); + case TokenType::LiteralOctal: + return std::stoi(token.substr(2), nullptr, 8); + case TokenType::LiteralNull: + return nullptr; + case TokenType::LiteralTemplate: + return std::string(token.substr(1, token.length() - 2)); + case TokenType::LiteralRegex: + return std::string(token.substr(1, token.length() - 2)); + case TokenType::LiteralDate: + return token; // Store as string, could be parsed later + case TokenType::LiteralBigInt: + return token.substr(0, token.length() - 1); // Remove 'n' suffix and store as string + default: + return token; + } + } catch (const std::exception& e) { + FABRIC_LOG_ERROR("Error parsing value: {}", e.what()); + return nullptr; } - } catch (const std::exception &e) { - FABRIC_LOG_ERROR("Error parsing value: {}", e.what()); - return nullptr; - } } } // namespace fabric diff --git a/src/ui/BgfxRenderInterface.cc b/src/ui/BgfxRenderInterface.cc index 2972583a..7d8f09f5 100644 --- a/src/ui/BgfxRenderInterface.cc +++ b/src/ui/BgfxRenderInterface.cc @@ -4,7 +4,15 @@ #include "stb_image.h" -// Suppress WGSL: bgfx CMake helpers don't compile it yet +// Suppress shader profiles we don't compile per-platform. +// bgfx's embedded_shader.h enables DXBC/DXIL on Windows and Linux, +// and WGSL broadly, but we only compile the profiles listed in +// FabricRmlUi.cmake: dxbc (Windows), glsl, essl, spv, mtl (macOS). +// DXIL (SM 6.x) is not compiled — suppress it everywhere. +#if !defined(_WIN32) +#define BGFX_PLATFORM_SUPPORTS_DXBC 0 +#endif +#define BGFX_PLATFORM_SUPPORTS_DXIL 0 #define BGFX_PLATFORM_SUPPORTS_WGSL 0 #include @@ -13,41 +21,38 @@ // Compiled shader bytecode generated at build time from .sc sources. // Each profile produces a separate header with a uint8_t array named // _ (e.g. vs_rmlui_mtl, fs_rmlui_glsl). -#include "glsl/vs_rmlui.sc.bin.h" -#include "glsl/fs_rmlui.sc.bin.h" -#include "essl/vs_rmlui.sc.bin.h" #include "essl/fs_rmlui.sc.bin.h" -#include "spv/vs_rmlui.sc.bin.h" +#include "essl/vs_rmlui.sc.bin.h" +#include "glsl/fs_rmlui.sc.bin.h" +#include "glsl/vs_rmlui.sc.bin.h" #include "spv/fs_rmlui.sc.bin.h" +#include "spv/vs_rmlui.sc.bin.h" +#if BX_PLATFORM_WINDOWS +#include "dxbc/fs_rmlui.sc.bin.h" +#include "dxbc/vs_rmlui.sc.bin.h" +#endif #if BX_PLATFORM_OSX || BX_PLATFORM_IOS || BX_PLATFORM_VISIONOS -#include "mtl/vs_rmlui.sc.bin.h" #include "mtl/fs_rmlui.sc.bin.h" +#include "mtl/vs_rmlui.sc.bin.h" #endif -static const bgfx::EmbeddedShader s_embeddedShaders[] = { - BGFX_EMBEDDED_SHADER(vs_rmlui), - BGFX_EMBEDDED_SHADER(fs_rmlui), - BGFX_EMBEDDED_SHADER_END() -}; +static const bgfx::EmbeddedShader s_embeddedShaders[] = {BGFX_EMBEDDED_SHADER(vs_rmlui), BGFX_EMBEDDED_SHADER(fs_rmlui), + BGFX_EMBEDDED_SHADER_END()}; namespace fabric { namespace { // Premultiplied alpha: SRC=ONE, DST=INV_SRC_ALPHA -constexpr uint64_t kRenderState = - BGFX_STATE_WRITE_RGB - | BGFX_STATE_WRITE_A - | BGFX_STATE_MSAA - | BGFX_STATE_BLEND_FUNC(BGFX_STATE_BLEND_ONE, BGFX_STATE_BLEND_INV_SRC_ALPHA); +constexpr uint64_t kRenderState = BGFX_STATE_WRITE_RGB | BGFX_STATE_WRITE_A | BGFX_STATE_MSAA | + BGFX_STATE_BLEND_FUNC(BGFX_STATE_BLEND_ONE, BGFX_STATE_BLEND_INV_SRC_ALPHA); } // namespace BgfxRenderInterface::BgfxRenderInterface() { bx::mtxIdentity(transform_); - layout_ - .begin() + layout_.begin() .add(bgfx::Attrib::Position, 2, bgfx::AttribType::Float) .add(bgfx::Attrib::Color0, 4, bgfx::AttribType::Uint8, true) .add(bgfx::Attrib::TexCoord0, 2, bgfx::AttribType::Float) @@ -60,20 +65,16 @@ void BgfxRenderInterface::init() { FABRIC_ZONE_SCOPED; bgfx::RendererType::Enum type = bgfx::getRendererType(); - program_ = bgfx::createProgram( - bgfx::createEmbeddedShader(s_embeddedShaders, type, "vs_rmlui"), - bgfx::createEmbeddedShader(s_embeddedShaders, type, "fs_rmlui"), - true); + program_ = bgfx::createProgram(bgfx::createEmbeddedShader(s_embeddedShaders, type, "vs_rmlui"), + bgfx::createEmbeddedShader(s_embeddedShaders, type, "fs_rmlui"), true); texUniform_ = bgfx::createUniform("s_tex", bgfx::UniformType::Sampler); // 1x1 white texture for untextured geometry uint32_t white = 0xFFFFFFFF; - whiteTexture_ = bgfx::createTexture2D( - 1, 1, false, 1, - bgfx::TextureFormat::RGBA8, - BGFX_SAMPLER_U_CLAMP | BGFX_SAMPLER_V_CLAMP, - bgfx::copy(&white, sizeof(white))); + whiteTexture_ = + bgfx::createTexture2D(1, 1, false, 1, bgfx::TextureFormat::RGBA8, BGFX_SAMPLER_U_CLAMP | BGFX_SAMPLER_V_CLAMP, + bgfx::copy(&white, sizeof(white))); FABRIC_LOG_INFO("RmlUi bgfx render interface initialized (view {})", viewId_); } @@ -92,13 +93,16 @@ void BgfxRenderInterface::shutdown() { } textures_.clear(); - if (bgfx::isValid(whiteTexture_)) bgfx::destroy(whiteTexture_); - if (bgfx::isValid(texUniform_)) bgfx::destroy(texUniform_); - if (bgfx::isValid(program_)) bgfx::destroy(program_); + if (bgfx::isValid(whiteTexture_)) + bgfx::destroy(whiteTexture_); + if (bgfx::isValid(texUniform_)) + bgfx::destroy(texUniform_); + if (bgfx::isValid(program_)) + bgfx::destroy(program_); whiteTexture_ = BGFX_INVALID_HANDLE; - texUniform_ = BGFX_INVALID_HANDLE; - program_ = BGFX_INVALID_HANDLE; + texUniform_ = BGFX_INVALID_HANDLE; + program_ = BGFX_INVALID_HANDLE; FABRIC_LOG_INFO("RmlUi bgfx render interface shut down"); } @@ -108,16 +112,8 @@ void BgfxRenderInterface::beginFrame(uint16_t width, uint16_t height) { float ortho[16]; const bgfx::Caps* caps = bgfx::getCaps(); - bx::mtxOrtho( - ortho, - 0.0f, - static_cast(width), - static_cast(height), - 0.0f, - 0.0f, - 1000.0f, - 0.0f, - caps->homogeneousDepth); + bx::mtxOrtho(ortho, 0.0f, static_cast(width), static_cast(height), 0.0f, 0.0f, 1000.0f, 0.0f, + caps->homogeneousDepth); bgfx::setViewTransform(viewId_, nullptr, ortho); bgfx::setViewRect(viewId_, 0, 0, width, height); @@ -128,19 +124,15 @@ void BgfxRenderInterface::beginFrame(uint16_t width, uint16_t height) { // -- Geometry -- -Rml::CompiledGeometryHandle BgfxRenderInterface::CompileGeometry( - Rml::Span vertices, - Rml::Span indices) -{ +Rml::CompiledGeometryHandle BgfxRenderInterface::CompileGeometry(Rml::Span vertices, + Rml::Span indices) { FABRIC_ZONE_SCOPED; CompiledGeom geom; geom.vbh = bgfx::createVertexBuffer( - bgfx::copy(vertices.data(), static_cast(vertices.size() * sizeof(Rml::Vertex))), - layout_); - geom.ibh = bgfx::createIndexBuffer( - bgfx::copy(indices.data(), static_cast(indices.size() * sizeof(int))), - BGFX_BUFFER_INDEX32); + bgfx::copy(vertices.data(), static_cast(vertices.size() * sizeof(Rml::Vertex))), layout_); + geom.ibh = bgfx::createIndexBuffer(bgfx::copy(indices.data(), static_cast(indices.size() * sizeof(int))), + BGFX_BUFFER_INDEX32); geom.indexCount = static_cast(indices.size()); auto handle = nextGeomHandle_++; @@ -148,15 +140,13 @@ Rml::CompiledGeometryHandle BgfxRenderInterface::CompileGeometry( return static_cast(handle); } -void BgfxRenderInterface::RenderGeometry( - Rml::CompiledGeometryHandle geometry, - Rml::Vector2f translation, - Rml::TextureHandle texture) -{ +void BgfxRenderInterface::RenderGeometry(Rml::CompiledGeometryHandle geometry, Rml::Vector2f translation, + Rml::TextureHandle texture) { FABRIC_ZONE_SCOPED; auto it = geometries_.find(static_cast(geometry)); - if (it == geometries_.end()) return; + if (it == geometries_.end()) + return; const auto& geom = it->second; @@ -187,11 +177,8 @@ void BgfxRenderInterface::RenderGeometry( // Per-draw-call scissor if (scissorEnabled_) { - bgfx::setScissor( - static_cast(scissorRect_.Left()), - static_cast(scissorRect_.Top()), - static_cast(scissorRect_.Width()), - static_cast(scissorRect_.Height())); + bgfx::setScissor(static_cast(scissorRect_.Left()), static_cast(scissorRect_.Top()), + static_cast(scissorRect_.Width()), static_cast(scissorRect_.Height())); } bgfx::setState(kRenderState); @@ -200,7 +187,8 @@ void BgfxRenderInterface::RenderGeometry( void BgfxRenderInterface::ReleaseGeometry(Rml::CompiledGeometryHandle geometry) { auto it = geometries_.find(static_cast(geometry)); - if (it == geometries_.end()) return; + if (it == geometries_.end()) + return; bgfx::destroy(it->second.vbh); bgfx::destroy(it->second.ibh); @@ -209,8 +197,7 @@ void BgfxRenderInterface::ReleaseGeometry(Rml::CompiledGeometryHandle geometry) // -- Textures -- -Rml::TextureHandle BgfxRenderInterface::LoadTexture(Rml::Vector2i& dimensions, - const Rml::String& source) { +Rml::TextureHandle BgfxRenderInterface::LoadTexture(Rml::Vector2i& dimensions, const Rml::String& source) { FABRIC_ZONE_SCOPED; int w = 0, h = 0, channels = 0; @@ -224,11 +211,9 @@ Rml::TextureHandle BgfxRenderInterface::LoadTexture(Rml::Vector2i& dimensions, dimensions.y = h; uint32_t size = static_cast(w * h * 4); - bgfx::TextureHandle tex = bgfx::createTexture2D( - static_cast(w), static_cast(h), - false, 1, bgfx::TextureFormat::RGBA8, - BGFX_SAMPLER_U_CLAMP | BGFX_SAMPLER_V_CLAMP, - bgfx::copy(data, size)); + bgfx::TextureHandle tex = + bgfx::createTexture2D(static_cast(w), static_cast(h), false, 1, bgfx::TextureFormat::RGBA8, + BGFX_SAMPLER_U_CLAMP | BGFX_SAMPLER_V_CLAMP, bgfx::copy(data, size)); stbi_image_free(data); @@ -237,20 +222,12 @@ Rml::TextureHandle BgfxRenderInterface::LoadTexture(Rml::Vector2i& dimensions, return static_cast(handle); } -Rml::TextureHandle BgfxRenderInterface::GenerateTexture( - Rml::Span source, - Rml::Vector2i dimensions) -{ +Rml::TextureHandle BgfxRenderInterface::GenerateTexture(Rml::Span source, Rml::Vector2i dimensions) { FABRIC_ZONE_SCOPED; bgfx::TextureHandle tex = bgfx::createTexture2D( - static_cast(dimensions.x), - static_cast(dimensions.y), - false, - 1, - bgfx::TextureFormat::RGBA8, - BGFX_SAMPLER_U_CLAMP | BGFX_SAMPLER_V_CLAMP, - bgfx::copy(source.data(), static_cast(source.size()))); + static_cast(dimensions.x), static_cast(dimensions.y), false, 1, bgfx::TextureFormat::RGBA8, + BGFX_SAMPLER_U_CLAMP | BGFX_SAMPLER_V_CLAMP, bgfx::copy(source.data(), static_cast(source.size()))); auto handle = nextTexHandle_++; textures_[handle] = tex; @@ -259,7 +236,8 @@ Rml::TextureHandle BgfxRenderInterface::GenerateTexture( void BgfxRenderInterface::ReleaseTexture(Rml::TextureHandle texture) { auto it = textures_.find(static_cast(texture)); - if (it == textures_.end()) return; + if (it == textures_.end()) + return; bgfx::destroy(it->second); textures_.erase(it); diff --git a/src/ui/BgfxSystemInterface.cc b/src/ui/BgfxSystemInterface.cc index b8f12afb..31587754 100644 --- a/src/ui/BgfxSystemInterface.cc +++ b/src/ui/BgfxSystemInterface.cc @@ -3,32 +3,30 @@ namespace fabric { -BgfxSystemInterface::BgfxSystemInterface() - : startTime_(std::chrono::steady_clock::now()) {} +BgfxSystemInterface::BgfxSystemInterface() : startTime_(std::chrono::steady_clock::now()) {} double BgfxSystemInterface::GetElapsedTime() { auto now = std::chrono::steady_clock::now(); return std::chrono::duration(now - startTime_).count(); } -bool BgfxSystemInterface::LogMessage(Rml::Log::Type type, - const Rml::String& message) { +bool BgfxSystemInterface::LogMessage(Rml::Log::Type type, const Rml::String& message) { switch (type) { - case Rml::Log::LT_ERROR: - case Rml::Log::LT_ASSERT: - FABRIC_LOG_ERROR("[RmlUi] {}", message); - break; - case Rml::Log::LT_WARNING: - FABRIC_LOG_WARN("[RmlUi] {}", message); - break; - case Rml::Log::LT_INFO: - FABRIC_LOG_INFO("[RmlUi] {}", message); - break; - case Rml::Log::LT_DEBUG: - case Rml::Log::LT_MAX: - case Rml::Log::LT_ALWAYS: - FABRIC_LOG_DEBUG("[RmlUi] {}", message); - break; + case Rml::Log::LT_ERROR: + case Rml::Log::LT_ASSERT: + FABRIC_LOG_ERROR("[RmlUi] {}", message); + break; + case Rml::Log::LT_WARNING: + FABRIC_LOG_WARN("[RmlUi] {}", message); + break; + case Rml::Log::LT_INFO: + FABRIC_LOG_INFO("[RmlUi] {}", message); + break; + case Rml::Log::LT_DEBUG: + case Rml::Log::LT_MAX: + case Rml::Log::LT_ALWAYS: + FABRIC_LOG_DEBUG("[RmlUi] {}", message); + break; } return true; } diff --git a/src/ui/WebView.cc b/src/ui/WebView.cc index 49725a65..455e3483 100644 --- a/src/ui/WebView.cc +++ b/src/ui/WebView.cc @@ -6,77 +6,73 @@ namespace fabric { -WebView::WebView(const std::string &title, int width, int height, bool debug, - bool createWindow, void *window) +WebView::WebView(const std::string& title, int width, int height, bool debug, bool createWindow, void* window) : title(title), width(width), height(height), debug(debug), html("") { - if (createWindow) { - webview_ = std::make_unique(debug, window); - webview_->set_title(title.c_str()); - webview_->set_size(width, height, WEBVIEW_HINT_NONE); - FABRIC_LOG_INFO("WebView created: {} ({}x{})", title, width, height); - } + if (createWindow) { + webview_ = std::make_unique(debug, window); + webview_->set_title(title.c_str()); + webview_->set_size(width, height, WEBVIEW_HINT_NONE); + FABRIC_LOG_INFO("WebView created: {} ({}x{})", title, width, height); + } } -void WebView::setTitle(const std::string &title) { - this->title = title; - if (webview_) { - webview_->set_title(title.c_str()); - } +void WebView::setTitle(const std::string& title) { + this->title = title; + if (webview_) { + webview_->set_title(title.c_str()); + } } void WebView::setSize(int width, int height, webview_hint_t hint) { - this->width = width; - this->height = height; - if (webview_) { - webview_->set_size(width, height, hint); - } + this->width = width; + this->height = height; + if (webview_) { + webview_->set_size(width, height, hint); + } } -void WebView::navigate(const std::string &url) { - if (webview_) { - webview_->navigate(url.c_str()); - FABRIC_LOG_INFO("WebView navigating to: {}", url); - } +void WebView::navigate(const std::string& url) { + if (webview_) { + webview_->navigate(url.c_str()); + FABRIC_LOG_INFO("WebView navigating to: {}", url); + } } -void WebView::setHTML(const std::string &html) { - this->html = html; - if (webview_) { - webview_->set_html(html.c_str()); - FABRIC_LOG_DEBUG("WebView HTML content set"); - } +void WebView::setHTML(const std::string& html) { + this->html = html; + if (webview_) { + webview_->set_html(html.c_str()); + FABRIC_LOG_DEBUG("WebView HTML content set"); + } } void WebView::run() { - if (webview_) { - FABRIC_LOG_INFO("Starting WebView main loop"); - webview_->run(); - } else { - FABRIC_LOG_WARN("Attempting to run a WebView that was not created"); - } + if (webview_) { + FABRIC_LOG_INFO("Starting WebView main loop"); + webview_->run(); + } else { + FABRIC_LOG_WARN("Attempting to run a WebView that was not created"); + } } void WebView::terminate() { - if (webview_) { - FABRIC_LOG_INFO("Terminating WebView"); - webview_->terminate(); - } + if (webview_) { + FABRIC_LOG_INFO("Terminating WebView"); + webview_->terminate(); + } } -void WebView::eval(const std::string &js) { - if (webview_) { - webview_->eval(js.c_str()); - } +void WebView::eval(const std::string& js) { + if (webview_) { + webview_->eval(js.c_str()); + } } -void WebView::bind(const std::string &name, - const std::function &fn) { - if (webview_) { - webview_->bind(name.c_str(), [fn](const std::string &req) -> std::string { - return fn(req); - }); - FABRIC_LOG_DEBUG("Bound JavaScript function: {}", name); - } +void WebView::bind(const std::string& name, const std::function& fn) { + if (webview_) { + webview_->bind(name.c_str(), [fn](const std::string& req) -> std::string { return fn(req); }); + FABRIC_LOG_DEBUG("Bound JavaScript function: {}", name); + } } } // namespace fabric diff --git a/src/utils/ErrorHandling.cc b/src/utils/ErrorHandling.cc index 7aca1f48..5f9c865b 100644 --- a/src/utils/ErrorHandling.cc +++ b/src/utils/ErrorHandling.cc @@ -3,40 +3,41 @@ namespace fabric { -FabricException::FabricException(const std::string &message) - : message(message) {} +FabricException::FabricException(const std::string& message) : message(message) {} -const char *FabricException::what() const noexcept { return message.c_str(); } +const char* FabricException::what() const noexcept { + return message.c_str(); +} -void throwError(const std::string &message) { - FABRIC_LOG_ERROR("FabricException: {}", message); - throw FabricException(message); +void throwError(const std::string& message) { + FABRIC_LOG_ERROR("FabricException: {}", message); + throw FabricException(message); } std::string_view errorCodeToString(ErrorCode code) { - switch (code) { - case ErrorCode::Ok: - return "Ok"; - case ErrorCode::BufferOverrun: - return "BufferOverrun"; - case ErrorCode::InvalidState: - return "InvalidState"; - case ErrorCode::Timeout: - return "Timeout"; - case ErrorCode::ConnectionReset: - return "ConnectionReset"; - case ErrorCode::PermissionDenied: - return "PermissionDenied"; - case ErrorCode::NotFound: - return "NotFound"; - case ErrorCode::AlreadyExists: - return "AlreadyExists"; - case ErrorCode::ResourceExhausted: - return "ResourceExhausted"; - case ErrorCode::Internal: - return "Internal"; - } - return "Unknown"; + switch (code) { + case ErrorCode::Ok: + return "Ok"; + case ErrorCode::BufferOverrun: + return "BufferOverrun"; + case ErrorCode::InvalidState: + return "InvalidState"; + case ErrorCode::Timeout: + return "Timeout"; + case ErrorCode::ConnectionReset: + return "ConnectionReset"; + case ErrorCode::PermissionDenied: + return "PermissionDenied"; + case ErrorCode::NotFound: + return "NotFound"; + case ErrorCode::AlreadyExists: + return "AlreadyExists"; + case ErrorCode::ResourceExhausted: + return "ResourceExhausted"; + case ErrorCode::Internal: + return "Internal"; + } + return "Unknown"; } } // namespace fabric diff --git a/src/utils/ThreadPoolExecutor.cc b/src/utils/ThreadPoolExecutor.cc index 3f91c713..c9283eac 100644 --- a/src/utils/ThreadPoolExecutor.cc +++ b/src/utils/ThreadPoolExecutor.cc @@ -13,7 +13,7 @@ ThreadPoolExecutor::ThreadPoolExecutor(size_t threadCount) for (size_t i = 0; i < threadCount_; ++i) { workerThreads_.emplace_back(&ThreadPoolExecutor::workerThread, this); } - + FABRIC_LOG_DEBUG("ThreadPoolExecutor created with {} threads", threadCount_.load()); } @@ -39,24 +39,24 @@ void ThreadPoolExecutor::setThreadCount(size_t count) { if (count == 0) { throw std::invalid_argument("Thread count must be at least 1"); } - + // Store the current thread count size_t oldCount = threadCount_; - + // Set the new thread count threadCount_ = count; - + // If we're reducing the thread count if (count < oldCount) { std::lock_guard lock(queueMutex_); - + // Notify worker threads that they should check their status queueCondition_.notify_all(); - + // The excess threads will exit naturally in workerThread() // when they recheck threadCount_ } - + // If we're increasing the thread count if (count > oldCount && !shutdown_ && !pausedForTesting_) { // Start new worker threads @@ -64,9 +64,8 @@ void ThreadPoolExecutor::setThreadCount(size_t count) { workerThreads_.emplace_back(&ThreadPoolExecutor::workerThread, this); } } - - FABRIC_LOG_DEBUG("ThreadPoolExecutor thread count changed from {} to {}", - oldCount, count); + + FABRIC_LOG_DEBUG("ThreadPoolExecutor thread count changed from {} to {}", oldCount, count); } size_t ThreadPoolExecutor::getThreadCount() const { @@ -92,8 +91,8 @@ bool ThreadPoolExecutor::shutdown(std::chrono::milliseconds timeout) { continue; } - auto elapsed = std::chrono::duration_cast( - std::chrono::steady_clock::now() - startTime); + auto elapsed = + std::chrono::duration_cast(std::chrono::steady_clock::now() - startTime); if (elapsed >= timeout) { // Time budget exhausted — detach remaining threads @@ -173,10 +172,10 @@ void ThreadPoolExecutor::resumeAfterTesting() { if (!pausedForTesting_) { return; } - + // Resume normal operation pausedForTesting_ = false; - + // Restart worker threads if needed if (!shutdown_ && workerThreads_.size() < threadCount_) { size_t threadsToStart = threadCount_ - workerThreads_.size(); @@ -184,7 +183,7 @@ void ThreadPoolExecutor::resumeAfterTesting() { workerThreads_.emplace_back(&ThreadPoolExecutor::workerThread, this); } } - + FABRIC_LOG_DEBUG("ThreadPoolExecutor resumed after testing"); } @@ -208,28 +207,28 @@ void ThreadPoolExecutor::workerThread() { break; } } - + // Check if this thread should exit due to thread count reduction if (threadIndex >= threadCount_) { break; } - + // Get a task from the queue Task task; bool hasTask = false; { std::unique_lock lock(queueMutex_); - + // Wait for a task or shutdown signal queueCondition_.wait(lock, [this, threadIndex] { return !taskQueue_.empty() || shutdown_ || pausedForTesting_ || threadIndex >= threadCount_; }); - + // Check for shutdown or thread count reduction if (shutdown_ || pausedForTesting_ || threadIndex >= threadCount_) { break; } - + // Get the task if (!taskQueue_.empty()) { task = std::move(taskQueue_.front()); @@ -237,7 +236,7 @@ void ThreadPoolExecutor::workerThread() { hasTask = true; } } - + // Execute the task if (hasTask) { try { @@ -253,4 +252,4 @@ void ThreadPoolExecutor::workerThread() { } } // namespace Utils -} // namespace fabric \ No newline at end of file +} // namespace fabric diff --git a/src/utils/Utils.cc b/src/utils/Utils.cc index b03a0fcd..b8a9e2ba 100644 --- a/src/utils/Utils.cc +++ b/src/utils/Utils.cc @@ -7,21 +7,21 @@ namespace fabric { std::string Utils::generateUniqueId(const std::string& prefix, int length) { - static std::mutex idMutex; - static std::random_device rd; - static std::mt19937 gen(rd()); - static std::uniform_int_distribution<> dis(0, 15); + static std::mutex idMutex; + static std::random_device rd; + static std::mt19937 gen(rd()); + static std::uniform_int_distribution<> dis(0, 15); - std::lock_guard lock(idMutex); + std::lock_guard lock(idMutex); - std::stringstream ss; - ss << prefix; + std::stringstream ss; + ss << prefix; - for (int i = 0; i < length; i++) { - ss << std::hex << dis(gen); - } + for (int i = 0; i < length; i++) { + ss << std::hex << dis(gen); + } - return ss.str(); + return ss.str(); } } // namespace fabric diff --git a/tasks/build.sh b/tasks/build.sh index fbdb6a3f..848f16bd 100755 --- a/tasks/build.sh +++ b/tasks/build.sh @@ -1,21 +1,16 @@ #!/bin/sh -# Configure and build Fabric Engine. -# Env: BUILD_TYPE - cmake build type (default: Debug) -# Env: BUILD_DIR - build output directory (default: build) -# Env: EXTRA_FLAGS - extra cmake flags +# Configure and build Fabric Engine using CMakePresets. +# Env: BUILD_PRESET - cmake preset name (default: dev-debug) +# See CMakePresets.json for available presets. set -eu -build_type="${BUILD_TYPE:-Debug}" -build_dir="${BUILD_DIR:-build}" +preset="${BUILD_PRESET:-dev-debug}" +build_dir="build/${preset}" if [ ! -f "${build_dir}/build.ninja" ]; then - echo "Configuring (${build_type})" - cmake -G Ninja \ - -DCMAKE_BUILD_TYPE="${build_type}" \ - -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ - ${EXTRA_FLAGS:-} \ - -B "${build_dir}" + echo "Configuring (${preset})" + cmake --preset "${preset}" fi -echo "Building (${build_type})" -cmake --build "${build_dir}" +echo "Building (${preset})" +cmake --build "${build_dir}" -j diff --git a/tasks/codeql.sh b/tasks/codeql.sh new file mode 100644 index 00000000..8dfa3f5e --- /dev/null +++ b/tasks/codeql.sh @@ -0,0 +1,67 @@ +#!/bin/sh +# Run CodeQL security analysis locally. +# Env: CODEQL_LANG - language to analyze (default: cpp) +# Env: CODEQL_SUITE - query suite (default: code-scanning) +# Env: BUILD_DIR - cmake build directory (default: build) +# Requires: codeql CLI (mise manages this) +set -eu + +lang="${CODEQL_LANG:-cpp}" +suite="${CODEQL_SUITE:-code-scanning}" +build_dir="${BUILD_DIR:-build}" +db_dir="build/codeql-db" + +if ! command -v codeql >/dev/null 2>&1; then + echo "codeql not found. Run: mise install" >&2 + exit 1 +fi + +echo "CodeQL $(codeql --version | head -1)" + +# Clean previous database +if [ -d "$db_dir" ]; then + echo "Removing previous database" + rm -rf "$db_dir" +fi + +# Create database (for compiled languages, codeql needs to observe the build) +echo "Creating CodeQL database (${lang})" +if [ "$lang" = "cpp" ] || [ "$lang" = "java" ] || [ "$lang" = "csharp" ] || [ "$lang" = "go" ] || [ "$lang" = "swift" ]; then + # Compiled language: codeql needs a build command + # Ensure clean build so codeql sees all compilations + codeql database create "$db_dir" \ + --language="$lang" \ + --command="cmake --build ${build_dir} --clean-first -j" \ + --overwrite +else + # Interpreted language: no build command needed + codeql database create "$db_dir" \ + --language="$lang" \ + --overwrite +fi + +# Run analysis +echo "Analyzing with suite: ${suite}" +results_file="build/codeql-results.sarif" +codeql database analyze "$db_dir" \ + --format=sarifv2.1.0 \ + --output="$results_file" \ + "${lang}-${suite}.qls" + +# Summary +echo "" +echo "Results: ${results_file}" + +# Count findings from SARIF +if command -v jq >/dev/null 2>&1; then + count=$(jq '[.runs[].results[]] | length' "$results_file" 2>/dev/null || echo "?") + echo "Findings: ${count}" + if [ "$count" != "0" ] && [ "$count" != "?" ]; then + echo "" + echo "Top findings:" + jq -r '.runs[].results[] | " \(.level // "warning"): \(.message.text) [\(.ruleId)]"' \ + "$results_file" 2>/dev/null | head -20 + fi +else + echo "(install jq for finding summary)" +fi diff --git a/tasks/coverage.sh b/tasks/coverage.sh new file mode 100755 index 00000000..277d1bff --- /dev/null +++ b/tasks/coverage.sh @@ -0,0 +1,42 @@ +#!/bin/sh +# Build with coverage instrumentation and generate lcov report. +# Requires: clang, llvm-profdata, llvm-cov +set -eu + +build_dir="build/ci-coverage" + +for tool in clang++ llvm-profdata llvm-cov; do + if ! command -v "$tool" >/dev/null 2>&1; then + echo "$tool not found. Install via: brew install llvm" >&2 + exit 1 + fi +done + +echo "Configuring (ci-coverage)" +cmake --preset ci-coverage + +echo "Building" +cmake --build "${build_dir}" -j + +echo "Running tests" +LLVM_PROFILE_FILE="${build_dir}/fabric-%p.profraw" \ + ctest --test-dir "${build_dir}" --output-on-failure + +echo "Merging profiles" +llvm-profdata merge -sparse "${build_dir}"/fabric-*.profraw -o "${build_dir}/coverage.profdata" + +echo "Generating lcov report" +llvm-cov export \ + "${build_dir}/bin/UnitTests" \ + -instr-profile="${build_dir}/coverage.profdata" \ + -format=lcov \ + -ignore-filename-regex='build/|_deps/|tests/' \ + > "${build_dir}/coverage.lcov" + +echo "Coverage report: ${build_dir}/coverage.lcov" + +# Show summary +llvm-cov report \ + "${build_dir}/bin/UnitTests" \ + -instr-profile="${build_dir}/coverage.profdata" \ + -ignore-filename-regex='build/|_deps/|tests/' diff --git a/tasks/cppcheck.sh b/tasks/cppcheck.sh new file mode 100755 index 00000000..f5ba293a --- /dev/null +++ b/tasks/cppcheck.sh @@ -0,0 +1,17 @@ +#!/bin/sh +# Run cppcheck static analysis on Fabric source files. +set -eu + +if ! command -v cppcheck >/dev/null 2>&1; then + echo "cppcheck not found. Install via: brew install cppcheck" >&2 + exit 1 +fi + +echo "Running cppcheck" +cppcheck \ + --enable=warning,performance,portability \ + --error-exitcode=1 \ + --suppress=missingInclude \ + --suppress=unmatchedSuppression \ + -I include/ \ + src/ include/ diff --git a/tasks/format.sh b/tasks/format.sh new file mode 100755 index 00000000..8904a37a --- /dev/null +++ b/tasks/format.sh @@ -0,0 +1,21 @@ +#!/bin/sh +# Check or fix clang-format on Fabric source files. +# Env: FORMAT_FIX - set to "1" to apply fixes (default: check-only) +set -eu + +fix_mode="${FORMAT_FIX:-}" + +if ! command -v clang-format >/dev/null 2>&1; then + echo "clang-format not found. Install via: brew install llvm" >&2 + exit 1 +fi + +if [ "$fix_mode" = "1" ] || [ "$fix_mode" = "true" ]; then + echo "Formatting source files" + find src include -name '*.cc' -o -name '*.hh' | xargs clang-format -i + echo "Done" +else + echo "Checking format (dry-run)" + find src include -name '*.cc' -o -name '*.hh' | xargs clang-format --dry-run --Werror + echo "All files formatted correctly" +fi diff --git a/tasks/lint.sh b/tasks/lint.sh index c351e1ea..612424d8 100755 --- a/tasks/lint.sh +++ b/tasks/lint.sh @@ -1,14 +1,15 @@ #!/bin/sh # Run clang-tidy on Fabric source files. -# Env: LINT_FIX - set to "1" to apply fixes (default: check-only) +# Env: LINT_FIX - set to "1" to apply fixes (CAUTION: can break cross-file refs) +# Env: LINT_CHANGED - set to "1" to only lint git-dirty files (fast) # Requires: compile_commands.json in build dir (cmake generates this) set -eu -build_dir="${BUILD_DIR:-build}" +build_dir="${BUILD_DIR:-build/dev-debug}" if [ ! -f "${build_dir}/compile_commands.json" ]; then - echo "No compile_commands.json found. Configuring build first." - cmake -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -B "${build_dir}" + echo "No compile_commands.json found. Running build first." + sh "$(dirname "$0")/build.sh" fi fix_flag="" @@ -22,13 +23,23 @@ if [ "$(uname)" = "Darwin" ] && command -v xcrun >/dev/null 2>&1; then sysroot_flag="--extra-arg=--sysroot=$(xcrun --show-sdk-path)" fi -echo "Running clang-tidy" +# Collect files to lint +if [ "${LINT_CHANGED:-}" = "1" ] || [ "${LINT_CHANGED:-}" = "true" ]; then + echo "Linting changed files only" + files=$(git diff --name-only --diff-filter=ACMR HEAD -- 'src/*.cc' 'src/*.hh' 'include/*.cc' 'include/*.hh' 2>/dev/null || true) + staged=$(git diff --cached --name-only --diff-filter=ACMR -- 'src/*.cc' 'src/*.hh' 'include/*.cc' 'include/*.hh' 2>/dev/null || true) + files=$(printf '%s\n%s' "$files" "$staged" | sort -u | grep -v 'Constants.g.hh' || true) + if [ -z "$files" ]; then + echo "No changed source files to lint" + exit 0 + fi +else + echo "Linting all source files" + files=$(find src include -name '*.cc' -o -name '*.hh' | grep -v 'Constants.g.hh') +fi -# Lint fabric sources only (skip deps, generated, tests) -find src include -name '*.cc' -o -name '*.hh' | \ - grep -v 'Constants.g.hh' | \ - xargs clang-tidy \ - -p "${build_dir}" \ - $sysroot_flag \ - $fix_flag \ - --quiet +echo "$files" | xargs clang-tidy \ + -p "${build_dir}" \ + $sysroot_flag \ + $fix_flag \ + --quiet 2>&1 | grep -v 'warnings generated' diff --git a/tasks/sanitize.sh b/tasks/sanitize.sh new file mode 100755 index 00000000..6a75a78c --- /dev/null +++ b/tasks/sanitize.sh @@ -0,0 +1,21 @@ +#!/bin/sh +# Build and test with AddressSanitizer + UndefinedBehaviorSanitizer. +# Requires: clang +set -eu + +preset="${SANITIZE_PRESET:-ci-sanitize}" +build_dir="build/${preset}" + +if ! command -v clang++ >/dev/null 2>&1; then + echo "clang++ not found. Install via: brew install llvm" >&2 + exit 1 +fi + +echo "Configuring (${preset})" +cmake --preset "${preset}" + +echo "Building" +cmake --build "${build_dir}" -j + +echo "Running tests with sanitizers" +ctest --test-dir "${build_dir}" --output-on-failure diff --git a/tasks/test.sh b/tasks/test.sh index 6fe26081..6a8a29a5 100755 --- a/tasks/test.sh +++ b/tasks/test.sh @@ -5,7 +5,7 @@ # Env: TEST_TIMEOUT - per-suite timeout in seconds (default: 120) set -eu -build_dir="${BUILD_DIR:-build}" +build_dir="${BUILD_DIR:-build/dev-debug}" suite="${TEST_SUITE:-unit}" timeout_sec="${TEST_TIMEOUT:-120}" diff --git a/tests/unit/core/ECSTest.cc b/tests/unit/core/ECSTest.cc index f3cf6999..d70de52f 100644 --- a/tests/unit/core/ECSTest.cc +++ b/tests/unit/core/ECSTest.cc @@ -2,7 +2,9 @@ #include "fabric/core/Spatial.hh" #include +#include #include +#include #include using namespace fabric; @@ -24,7 +26,7 @@ TEST(ECSTest, WorldMoveConstruction) { auto found = moved.get().lookup("test_entity"); EXPECT_TRUE(found.is_valid()); - const auto* pos = found.get(); + const auto* pos = found.try_get(); ASSERT_NE(pos, nullptr); EXPECT_FLOAT_EQ(pos->x, 1.0f); } @@ -72,13 +74,13 @@ TEST(ECSTest, EntityCreationWithComponents) { EXPECT_TRUE(entity.has()); EXPECT_TRUE(entity.has()); - const auto* pos = entity.get(); + const auto* pos = entity.try_get(); ASSERT_NE(pos, nullptr); EXPECT_FLOAT_EQ(pos->x, 1.0f); EXPECT_FLOAT_EQ(pos->y, 2.0f); EXPECT_FLOAT_EQ(pos->z, 3.0f); - const auto* rot = entity.get(); + const auto* rot = entity.try_get(); ASSERT_NE(rot, nullptr); EXPECT_FLOAT_EQ(rot->w, 1.0f); } @@ -91,7 +93,7 @@ TEST(ECSTest, EntityWithBoundingBox) { .set({0.0f, 0.0f, 0.0f}) .set({-1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f}); - const auto* bb = entity.get(); + const auto* bb = entity.try_get(); ASSERT_NE(bb, nullptr); EXPECT_FLOAT_EQ(bb->minX, -1.0f); EXPECT_FLOAT_EQ(bb->maxX, 1.0f); @@ -277,19 +279,19 @@ TEST(ECSTest, CreateSceneEntity) { EXPECT_TRUE(entity.has()); // Default position is origin - const auto* pos = entity.get(); + const auto* pos = entity.try_get(); ASSERT_NE(pos, nullptr); EXPECT_FLOAT_EQ(pos->x, 0.0f); EXPECT_FLOAT_EQ(pos->y, 0.0f); EXPECT_FLOAT_EQ(pos->z, 0.0f); // Default rotation is identity quaternion - const auto* rot = entity.get(); + const auto* rot = entity.try_get(); ASSERT_NE(rot, nullptr); EXPECT_FLOAT_EQ(rot->w, 1.0f); // Default scale is uniform 1 - const auto* scl = entity.get(); + const auto* scl = entity.try_get(); ASSERT_NE(scl, nullptr); EXPECT_FLOAT_EQ(scl->x, 1.0f); EXPECT_FLOAT_EQ(scl->y, 1.0f); @@ -347,7 +349,7 @@ TEST(ECSTest, UpdateTransformsRootEntity) { world.updateTransforms(); - const auto* ltw = root.get(); + const auto* ltw = root.try_get(); ASSERT_NE(ltw, nullptr); float x, y, z; extractTranslation(*ltw, x, y, z); @@ -369,7 +371,7 @@ TEST(ECSTest, UpdateTransformsParentChild) { world.updateTransforms(); // Child world position should be parent + child = (5, 3, 0) - const auto* ltw = child.get(); + const auto* ltw = child.try_get(); ASSERT_NE(ltw, nullptr); float x, y, z; extractTranslation(*ltw, x, y, z); @@ -394,7 +396,7 @@ TEST(ECSTest, UpdateTransformsThreeLevels) { world.updateTransforms(); // Child world position: (1, 2, 3) - const auto* ltw = child.get(); + const auto* ltw = child.try_get(); ASSERT_NE(ltw, nullptr); float x, y, z; extractTranslation(*ltw, x, y, z); @@ -411,7 +413,7 @@ TEST(ECSTest, UpdateTransformsRotationPropagation) { auto parent = world.createSceneEntity("parent"); auto q = Quaternion::fromAxisAngle( Vector3(0.0f, 1.0f, 0.0f), - static_cast(M_PI / 2.0)); + static_cast(std::numbers::pi / 2.0)); parent.set({q.x, q.y, q.z, q.w}); // Child at local position (1, 0, 0) @@ -421,7 +423,7 @@ TEST(ECSTest, UpdateTransformsRotationPropagation) { world.updateTransforms(); // 90 degrees Y rotation maps (1,0,0) -> (0,0,-1) - const auto* ltw = child.get(); + const auto* ltw = child.try_get(); ASSERT_NE(ltw, nullptr); float x, y, z; extractTranslation(*ltw, x, y, z); diff --git a/tests/unit/core/SceneViewTest.cc b/tests/unit/core/SceneViewTest.cc index 4c6f22c3..21cc1829 100644 --- a/tests/unit/core/SceneViewTest.cc +++ b/tests/unit/core/SceneViewTest.cc @@ -296,7 +296,7 @@ TEST(BoundingBoxComponentTest, SetAndGetBoundingBox) { auto entity = world.createSceneEntity("test"); entity.set({-1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f}); - const auto* bb = entity.get(); + const auto* bb = entity.try_get(); ASSERT_NE(bb, nullptr); EXPECT_FLOAT_EQ(bb->minX, -1.0f); EXPECT_FLOAT_EQ(bb->maxX, 1.0f); diff --git a/tests/unit/core/SpatialTest.cc b/tests/unit/core/SpatialTest.cc index 610bc082..eddc3dac 100644 --- a/tests/unit/core/SpatialTest.cc +++ b/tests/unit/core/SpatialTest.cc @@ -3,6 +3,7 @@ #include "fabric/utils/ErrorHandling.hh" #include #include +#include using namespace fabric; @@ -169,7 +170,7 @@ TEST_F(SpatialTest, QuaternionBasics) { TEST_F(SpatialTest, QuaternionRotation) { // Create a quaternion for 90 degrees rotation around Z axis Vector3 axis(0.0f, 0.0f, 1.0f); - float angle = M_PI / 2; // 90 degrees + float angle = std::numbers::pi / 2; // 90 degrees Quaternion qRot = Quaternion::fromAxisAngle(axis, angle); // Rotate a vector with this quaternion (should rotate x into y) @@ -296,7 +297,7 @@ TEST_F(SpatialTest, TransformBasics) { // Set custom transform Vector3 position(1.0f, 2.0f, 3.0f); Vector3 rotAxis(0.0f, 1.0f, 0.0f); - Quaternion rotation = Quaternion::fromAxisAngle(rotAxis, M_PI / 2); + Quaternion rotation = Quaternion::fromAxisAngle(rotAxis, std::numbers::pi / 2); Vector3 scale(2.0f, 2.0f, 2.0f); transform.setPosition(position); @@ -322,7 +323,7 @@ TEST_F(SpatialTest, TransformPointAndDirection) { // Create a transform that scales, rotates, and translates Vector3 position(0.0f, 1.0f, 0.0f); Vector3 rotAxis(0.0f, 0.0f, 1.0f); - Quaternion rotation = Quaternion::fromAxisAngle(rotAxis, M_PI / 2); // 90 degrees around Z + Quaternion rotation = Quaternion::fromAxisAngle(rotAxis, std::numbers::pi / 2); // 90 degrees around Z Vector3 scale(2.0f, 2.0f, 2.0f); Transform transform; @@ -367,7 +368,7 @@ TEST_F(SpatialTest, QuaternionSlerp) { // Endpoints: t=0 returns a, t=1 returns b Vector3 axis(0.0f, 0.0f, 1.0f); Quaternion a = Quaternion::fromAxisAngle(axis, 0.0f); - Quaternion b = Quaternion::fromAxisAngle(axis, static_cast(M_PI) / 2.0f); + Quaternion b = Quaternion::fromAxisAngle(axis, static_cast(std::numbers::pi) / 2.0f); auto at0 = Quaternion::slerp(a, b, 0.0f); EXPECT_TRUE(almostEqual(at0.x, a.x)); @@ -383,7 +384,7 @@ TEST_F(SpatialTest, QuaternionSlerp) { // Midpoint: t=0.5 for 90-degree rotation should give 45-degree rotation auto mid = Quaternion::slerp(a, b, 0.5f); - Quaternion expected45 = Quaternion::fromAxisAngle(axis, static_cast(M_PI) / 4.0f); + Quaternion expected45 = Quaternion::fromAxisAngle(axis, static_cast(std::numbers::pi) / 4.0f); EXPECT_TRUE(almostEqual(mid.x, expected45.x)); EXPECT_TRUE(almostEqual(mid.y, expected45.y)); EXPECT_TRUE(almostEqual(mid.z, expected45.z));